package com.ustadmobile.libuicompose.components import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.OutlinedTextField import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.withStyle import com.ustadmobile.core.util.MS_PER_HOUR import com.ustadmobile.core.util.MS_PER_MIN import kotlin.math.min class TimeVisualTransformation: VisualTransformation { private val mask = "hhmm" override fun filter(text: AnnotatedString): TransformedText { val trimmed = if(text.length >= mask.length) text.substring(mask.indices) else text.text val output = buildAnnotatedString { for(i in mask.indices) { if (i < trimmed.length) { append(trimmed[i]) }else { withStyle(style = SpanStyle(color = Color.LightGray)) { append(mask[i]) } } if(i == 1){ withStyle(style = SpanStyle(color = Color.LightGray)) { append(":") } } } } val offsetmapping= object: OffsetMapping { override fun originalToTransformed(offset: Int): Int { return if(offset <= 2) { offset }else if(offset <= mask.length) { offset + 1 }else { mask.length + 1 //text length plus ":" } } override fun transformedToOriginal(offset: Int): Int { val delta = if(offset <= 3) { offset }else if(offset <= mask.length + 1) { offset - 1 }else { mask.length } return min(delta, trimmed.length) } } return TransformedText(output, offsetmapping) } } @Composable fun UstadTimeField( value: Int, label: @Composable () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, isError: Boolean = false, onValueChange: (Int) -> Unit = {}, supportingText: (@Composable () -> Unit)? = null, ) { fun Int.toTimeString() : String{ return if(this > 0) { val hours = (this / MS_PER_HOUR) val mins = (this.mod(MS_PER_HOUR)) / MS_PER_MIN "${hours.toString().padStart(2, '0')}${mins.toString().padStart(2, '0')}" }else { "" } } var rawValue: String by remember(value) { mutableStateOf(value.toTimeString()) } OutlinedTextField( value = rawValue, label = label, modifier = modifier, enabled = enabled, isError = isError, onValueChange = { newValue -> val filtered = newValue.filter { it.isDigit() } rawValue = filtered.substring(0, min(filtered.length, 4)) if(filtered.length == 4) { try { val hours = filtered.substring(0, 2).toInt() val mins = filtered.substring(2, 4).toInt() if(hours >= 0 && hours <= 23 && mins >= 0 && mins <= 59) onValueChange((hours * MS_PER_HOUR) + (mins * MS_PER_MIN)) }catch(e: Exception) { //something not a valid time } }else if(filtered.isEmpty()) { onValueChange(0) } }, trailingIcon = { UstadTimePickerAdornment(onTimeSelected = { onValueChange(it) }) }, visualTransformation = TimeVisualTransformation(), singleLine = true, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), supportingText = supportingText, ) }