mirror of
https://github.com/Myzel394/NumberHub.git
synced 2025-06-19 08:45:27 +02:00
Text fields refactor
This commit is contained in:
parent
fec626eb57
commit
bd5cce157d
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String, InlineTextContent> = 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 <E> 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))
|
||||||
|
}
|
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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.Density
|
||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
@ -45,6 +45,8 @@ fun String.tokenLengthAhead(pos: Int): Int {
|
|||||||
if (pos.isAfterToken(this, it)) return it.length
|
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
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +42,7 @@ import androidx.compose.ui.text.TextRange
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.sadellie.unitto.core.ui.common.textfield.texttoolbar.UnittoTextToolbar
|
||||||
import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
|
import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -18,19 +18,41 @@
|
|||||||
|
|
||||||
package com.sadellie.unitto.core.ui.common.textfield
|
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.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.platform.ClipboardManager
|
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
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.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.TextRange
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
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
|
import com.sadellie.unitto.core.ui.theme.LocalNumberTypography
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -141,29 +163,95 @@ fun UnformattedTextField(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copy value to clipboard without grouping symbols.
|
* Based on: https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300
|
||||||
*
|
*
|
||||||
* Example:
|
* @param placeholder Placeholder text, shown when [value] is empty.
|
||||||
* "123.456,789" will be copied as "123456,789"
|
* @param textToolbar [TextToolbar] with modified actions in menu.
|
||||||
*
|
* @param alignment The alignment of the text within its container.
|
||||||
* @param value Formatted value that has grouping symbols.
|
* @see [BasicTextField]
|
||||||
|
* @see [AutoSizeTextStyleBox]
|
||||||
*/
|
*/
|
||||||
fun ClipboardManager.copyWithoutGrouping(
|
@Composable
|
||||||
|
private fun AutoSizeTextField(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
value: TextFieldValue,
|
value: TextFieldValue,
|
||||||
formatterSymbols: FormatterSymbols
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
) = this.setText(
|
placeholder: String? = null,
|
||||||
AnnotatedString(
|
textToolbar: TextToolbar = LocalTextToolbar.current,
|
||||||
value.annotatedString
|
enabled: Boolean = true,
|
||||||
.subSequence(value.selection)
|
readOnly: Boolean = false,
|
||||||
.text
|
textStyle: TextStyle = TextStyle.Default,
|
||||||
.replace(formatterSymbols.grouping, "")
|
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(
|
BasicTextField(
|
||||||
AnnotatedString(
|
value = value,
|
||||||
value.annotatedString
|
onValueChange = onValueChange,
|
||||||
.subSequence(value.selection)
|
modifier = Modifier
|
||||||
.text
|
.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()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -111,13 +111,7 @@ fun TextFieldValue.deleteTokens(): TextFieldValue {
|
|||||||
// We don't have anything selected (cursor in one position)
|
// We don't have anything selected (cursor in one position)
|
||||||
// like this 1234|56 => after deleting will be like this 123|56
|
// like this 1234|56 => after deleting will be like this 123|56
|
||||||
// Cursor moved one symbol left
|
// Cursor moved one symbol left
|
||||||
selection.start -> {
|
selection.start -> selection.start - text.tokenLengthAhead(selection.end)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
// We have multiple symbols selected
|
// We have multiple symbols selected
|
||||||
// like this 123[45]6 => after deleting will be like this 123|6
|
// like this 123[45]6 => after deleting will be like this 123|6
|
||||||
// Cursor will be placed where selection start was
|
// 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 {
|
private fun TextFieldValue.deleteAheadAndAdd(tokens: String): TextFieldValue {
|
||||||
var newValue = this
|
var newValue = this
|
||||||
|
// For cases like: "12+[34]" and "*" symbol is being added. Will delete selected tokens
|
||||||
if (!selection.collapsed) newValue = this.deleteTokens()
|
if (!selection.collapsed) newValue = this.deleteTokens()
|
||||||
return newValue
|
return newValue
|
||||||
.deleteTokens()
|
.deleteTokens()
|
||||||
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<TextUnit>.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<List<TextUnit>> = emptyList<TextUnit>().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<TextUnit>.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<List<TextUnit>> = emptyList<TextUnit>().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<TextUnit> {
|
|
||||||
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<TextUnit>.findElectedIndex(
|
|
||||||
shouldMoveBackward: (TextUnit) -> TextLayoutResult
|
|
||||||
): Pair<Int, TextLayoutResult?> {
|
|
||||||
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<TextUnit>.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.sadellie.unitto.core.ui.common.textfield.autosize
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
@Immutable
|
|
||||||
internal data class ImmutableWrapper<T>(val value: T)
|
|
||||||
|
|
||||||
internal fun <T> T.toImmutableWrapper() = ImmutableWrapper(this)
|
|
||||||
|
|
||||||
internal operator fun <T> ImmutableWrapper<T>.getValue(thisRef: Any?, property: KProperty<*>) = value
|
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Unitto is a unit converter for Android
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -16,15 +16,16 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
@RequiresApi(23)
|
@RequiresApi(Build.VERSION_CODES.M)
|
||||||
internal class FloatingTextActionModeCallback(
|
internal class FloatingTextActionModeCallback(
|
||||||
private val callback: UnittoActionModeCallback
|
private val callback: UnittoActionModeCallback
|
||||||
) : ActionMode.Callback2() {
|
) : ActionMode.Callback2() {
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Unitto is a unit converter for Android
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.sadellie.unitto.core.ui.common.textfield
|
package com.sadellie.unitto.core.ui.common.textfield.texttoolbar
|
||||||
|
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Unitto is a unit converter for Android
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.sadellie.unitto.core.ui.common.textfield
|
package com.sadellie.unitto.core.ui.common.textfield.texttoolbar
|
||||||
|
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
||||||
import android.view.Menu
|
import android.view.Menu
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Unitto is a unit converter for Android
|
* 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
|
* 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
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -16,7 +16,7 @@
|
|||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package com.sadellie.unitto.core.ui.common.textfield
|
package com.sadellie.unitto.core.ui.common.textfield.texttoolbar
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.view.ActionMode
|
import android.view.ActionMode
|
Loading…
x
Reference in New Issue
Block a user