Coverage Summary for Class: NumberInputFieldKt (com.vsevolodganin.clicktrack.ui.piece)

Class Method, % Branch, % Line, % Instruction, %
NumberInputFieldKt 0% (0/12) 0% (0/78) 0% (0/92) 0% (0/1311)
NumberInputFieldKt$NumberInputField$3$1$1 0% (0/1) 0% (0/2) 0% (0/18)
Total 0% (0/13) 0% (0/78) 0% (0/94) 0% (0/1329)


 package com.vsevolodganin.clicktrack.ui.piece
 
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.text.BasicTextField
 import androidx.compose.foundation.text.KeyboardActions
 import androidx.compose.foundation.text.KeyboardOptions
 import androidx.compose.material.LocalContentColor
 import androidx.compose.material.LocalTextStyle
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
 import androidx.compose.runtime.rememberCoroutineScope
 import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.focus.onFocusChanged
 import androidx.compose.ui.graphics.SolidColor
 import androidx.compose.ui.platform.LocalFocusManager
 import androidx.compose.ui.text.TextRange
 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.unit.dp
 import com.vsevolodganin.clicktrack.utils.compose.Preview
 import kotlinx.coroutines.launch
 import kotlin.math.absoluteValue
 
 @Composable
 fun NumberInputField(
     state: MutableState<Int>,
     modifier: Modifier = Modifier,
     isError: Boolean = false,
     showSign: Boolean = false,
     allowedNumbersRange: IntRange = DEFAULT_ALLOWED_NUMBERS_RANGE,
     fallbackNumber: Int? = null,
     visualTransformation: VisualTransformation = VisualTransformation.None,
 ) {
     NumberInputField(
         value = state.value,
         onValueChange = { state.value = it },
         modifier = modifier,
         isError = isError,
         showSign = showSign,
         allowedNumbersRange = allowedNumbersRange,
         fallbackNumber = fallbackNumber,
         visualTransformation = visualTransformation,
     )
 }
 
 @Composable
 fun NumberInputField(
     value: Int,
     onValueChange: (Int) -> Unit,
     modifier: Modifier = Modifier,
     isError: Boolean = false,
     showSign: Boolean = false,
     allowedNumbersRange: IntRange = DEFAULT_ALLOWED_NUMBERS_RANGE,
     fallbackNumber: Int? = null,
     visualTransformation: VisualTransformation = VisualTransformation.None,
 ) {
     val valueCoerced = value.coerceIn(allowedNumbersRange)
 
     var textFieldValue by remember(valueCoerced) {
         val text = valueCoerced.toText(showSign)
         mutableStateOf(
             TextFieldValue(
                 text = text,
                 selection = TextRange(text.length),
             ),
         )
     }
 
     val focusManager = LocalFocusManager.current
     var isFocused by remember { mutableStateOf(false) }
 
     val coroutineDispatcher = rememberCoroutineScope()
 
     BasicTextField(
         value = textFieldValue,
         onValueChange = { newValue ->
             val newText = newValue.text
 
             val newInt = when {
                 isIntermediateEditText(newText, showSign) -> {
                     textFieldValue = newValue.copy(text = newText)
                     return@BasicTextField
                 }
                 else -> newText.toIntOrNull()
             }
 
             newInt
                 ?.takeIf { it in allowedNumbersRange }
                 ?: return@BasicTextField
 
             textFieldValue = newValue.copy(text = newInt.toText(showSign))
 
             if (valueCoerced != newInt) {
                 onValueChange(newInt)
             }
         },
         modifier = modifier
             .focusableBorder(isError = isError)
             .onFocusChanged {
                 if (isFocused == it.isFocused) {
                     return@onFocusChanged
                 }
 
                 isFocused = it.isFocused
 
                 if (isFocused) {
                     // FIXME: Need to have a better way to set selection on the next frame
                     // because onValueChange rewrites our effort
                     coroutineDispatcher.launch {
                         textFieldValue = textFieldValue.copy(
                             selection = TextRange(0, textFieldValue.text.length),
                         )
                     }
                 } else if (isIntermediateEditText(textFieldValue.text, showSign)) {
                     if (fallbackNumber != null && fallbackNumber != valueCoerced) {
                         onValueChange(fallbackNumber)
                     }
                     textFieldValue = textFieldValue.copy(
                         text = valueCoerced.toText(showSign),
                     )
                 }
             }
             .padding(8.dp),
         cursorBrush = SolidColor(LocalContentColor.current),
         textStyle = LocalTextStyle.current
             .copy(
                 color = LocalContentColor.current,
                 textAlign = TextAlign.Center,
             ),
         keyboardOptions = KeyboardOptions(
             keyboardType = KeyboardType.Number,
             imeAction = ImeAction.Done,
         ),
         keyboardActions = KeyboardActions(
             onDone = { focusManager.clearFocus() },
         ),
         singleLine = true,
         maxLines = 1,
         visualTransformation = visualTransformation,
     )
 }
 
 private fun isIntermediateEditText(text: String, showSign: Boolean): Boolean {
     return text in if (showSign) {
         listOf("-", "+", "")
     } else {
         listOf("")
     }
 }
 
 private fun Int.toText(showSign: Boolean): String {
     return if (showSign) {
         if (this < 0) {
             "-${this.absoluteValue}"
         } else {
             "+$this"
         }
     } else {
         this.toString()
     }
 }
 
 private val DEFAULT_ALLOWED_NUMBERS_RANGE = 0..999999
 
 @Preview
 @Composable
 private fun Preview() {
     Column {
         NumberInputField(state = remember { mutableStateOf(666) })
         NumberInputField(state = remember { mutableStateOf(1666) }, allowedNumbersRange = -999..999)
     }
 }