```
private fun filterNumericInput(text: String): String =
    text.filterNot { it.isWhitespace() }.filter { it.isDigit() }

private fun filterRangeInput(text: String): String {
    val filtered = text.filterNot { it.isWhitespace() }
    return when {
        filtered.isEmpty() -> filtered
        filtered.first() == '-' -> "-${filtered.substring(1).filter { it.isDigit() }}"
        else -> filtered.filter { it.isDigit() }
    }
}

private fun filterNoWhitespace(text: String): String = text.filterNot { it.isWhitespace() }

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun GeneratorOptionsScreen(
    generatorType: GeneratorType,
    onNavigateBack: () -> Unit,
    modifier: Modifier = Modifier,
    viewModel: GeneratorOptionsViewModel = hiltViewModel()
) {
    val uiState by viewModel.getUiState().collectAsState()
    val titleResId =
        when (generatorType) {
            GeneratorType.NUM -> R.string.gen_title_num
            GeneratorType.ALPHANUM -> R.string.gen_title_alphanum
            GeneratorType.NUM_RANGE -> R.string.gen_title_num_range
            GeneratorType.NUM_FIXED -> R.string.gen_title_num_fixed
            GeneratorType.HEX -> R.string.gen_title_hex
        }

    LaunchedEffect(generatorType) {
        viewModel.loadOptions(generatorType)
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = {
                    Text(
                        stringResource(
                            R.string.generator_options_title,
                            stringResource(titleResId)
                        )
                    )
                },
                navigationIcon = {
                    IconButton(onClick = onNavigateBack) {
                        Icon(
                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,
                            contentDescription = stringResource(R.string.cd_navigate_back)
                        )
                    }
                }
            )
        },
        modifier = modifier
    ) { scaffoldPadding ->
        Column(
            modifier =
                Modifier
                    .fillMaxSize()
                    .padding(scaffoldPadding)
                    .padding(16.dp)
        ) {
            uiState.options?.let { options ->
                GeneratorOptionsContent(
                    options = options,
                    validationError = uiState.validationError,
                    onUpdateOptions = viewModel::saveOptions,
                    onClearError = viewModel::clearError
                )
            }
        }
    }
}

@Composable
private fun SaveButton(onClick: () -> Unit) {
    IconButton(
        onClick = onClick,
        modifier =
            Modifier
                .padding(end = 8.dp)
                .clip(CircleShape)
                .background(MaterialTheme.colorScheme.primary)
                .size(40.dp)
    ) {
        Icon(
            imageVector = Icons.Default.Done,
            contentDescription = null,
            tint = MaterialTheme.colorScheme.onPrimary
        )
    }
}

@Composable
private fun SupportingErrorText(validationError: ValidationErrorViewData) {
    when (validationError.reason) {
        Reason.ZeroOrLessMax ->
            Text(text = stringResource(R.string.error_gen_zero_or_less_max_msg))
        is Reason.LengthExcess ->
            Text(
                text =
                    stringResource(
                        R.string.error_gen_length_excess_msg_format,
                        validationError.reason.maxLength
                    )
            )
        Reason.ZeroOrLessLength ->
            Text(text = stringResource(R.string.error_gen_zero_or_less_length_msg))
        Reason.InvalidRange ->
            Text(text = stringResource(R.string.error_gen_invalid_range_msg))
        Reason.Unknown ->
            Text(text = stringResource(R.string.error_gen_unknown_msg))
        Reason.InvalidFormat ->
            Text(text = stringResource(R.string.error_gen_invalid_argument_format_msg))
        Reason.None -> Unit
    }
}

@Composable
private fun ValidatedOutlinedTextField(
    value: String,
    originalValue: String,
    onValueChange: (String) -> Unit,
    modifier: Modifier = Modifier,
    label: @Composable () -> Unit,
    validationError: ValidationErrorViewData?,
    isErrorApplicable: () -> Boolean,
    keyboardOptions: KeyboardOptions,
    onSave: () -> Unit,
    onClearError: () -> Unit
) {
    val errorApplicable = isErrorApplicable()
    val isErrorState = validationError != null && errorApplicable
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        modifier =
            modifier.onFocusChanged { focusState ->
                if (!focusState.isFocused) {
                    if (value != originalValue) {
                        onValueChange(originalValue)
                    }
                    if (isErrorState) {
                        onClearError()
                    }
                }
            },
        label = label,
        shape = MaterialTheme.shapes.extraLarge,
        trailingIcon = {
            if (value != originalValue) {
                SaveButton(onClick = onSave)
            }
        },
        keyboardOptions = keyboardOptions,
        singleLine = true,
        isError = isErrorState,
        supportingText = {
            if (validationError != null && errorApplicable) {
                SupportingErrorText(validationError)
            } else {
                Spacer(modifier = Modifier.height(16.dp))
            }
        }
    )
}

@Composable
private fun GeneratorOptionsContent(
    options: GeneratorOptions,
    validationError: ValidationErrorViewData?,
    onUpdateOptions: (GeneratorOptionsUpdateViewData) -> Unit,
    onClearError: () -> Unit
) {
    val modifier =
        Modifier
            .padding(vertical = 8.dp)
            .fillMaxWidth()

    val numberKeyboardOptions =
        KeyboardOptions(
            keyboardType = KeyboardType.Number,
            imeAction = ImeAction.Done
        )
    val textKeyboardOptions =
        KeyboardOptions(
            keyboardType = KeyboardType.Text,
            imeAction = ImeAction.Done
        )

    when (options) {
        is NumGeneratorOptions -> {
            var maxText by rememberSaveable { mutableStateOf(options.max.toString()) }
            ValidatedOutlinedTextField(
                value = maxText,
                originalValue = options.max.toString(),
                onValueChange = { maxText = filterNumericInput(it).take(MAX_INPUT_LENGTH) },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_max_value)) },
                validationError = validationError,
                isErrorApplicable = { validationError?.optionsUpdate is NumGeneratorMaxUpdate },
                keyboardOptions = numberKeyboardOptions,
                onSave = { onUpdateOptions(NumGeneratorMaxUpdate(maxText)) },
                onClearError = onClearError
            )
        }
        is NumFixedGeneratorOptions -> {
            var lengthText by rememberSaveable { mutableStateOf(options.length.toString()) }
            ValidatedOutlinedTextField(
                value = lengthText,
                originalValue = options.length.toString(),
                onValueChange = { lengthText = filterNumericInput(it).take(MAX_INPUT_LENGTH) },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_max_length)) },
                validationError = validationError,
                isErrorApplicable = {
                    validationError?.optionsUpdate is NumFixedGeneratorLengthUpdate
                },
                keyboardOptions = numberKeyboardOptions,
                onSave = { onUpdateOptions(NumFixedGeneratorLengthUpdate(lengthText)) },
                onClearError = onClearError
            )
        }
        is HexGeneratorOptions -> {
            var lengthText by rememberSaveable { mutableStateOf(options.length.toString()) }
            ValidatedOutlinedTextField(
                value = lengthText,
                originalValue = options.length.toString(),
                onValueChange = { lengthText = filterNumericInput(it).take(MAX_INPUT_LENGTH) },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_max_length)) },
                validationError = validationError,
                isErrorApplicable = { validationError?.optionsUpdate is HexGeneratorLengthUpdate },
                keyboardOptions = numberKeyboardOptions,
                onSave = { onUpdateOptions(HexGeneratorLengthUpdate(lengthText)) },
                onClearError = onClearError
            )
        }
        is NumRangeGeneratorOptions -> {
            var minText by rememberSaveable { mutableStateOf(options.min.toString()) }
            var maxText by rememberSaveable { mutableStateOf(options.max.toString()) }
            ValidatedOutlinedTextField(
                value = minText,
                originalValue = options.min.toString(),
                onValueChange = { minText = filterRangeInput(it).take(MAX_INPUT_LENGTH) },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_min_value)) },
                validationError = validationError,
                isErrorApplicable = {
                    validationError?.optionsUpdate is NumRangeGeneratorMinUpdate
                },
                keyboardOptions = numberKeyboardOptions,
                onSave = { onUpdateOptions(NumRangeGeneratorMinUpdate(minText)) },
                onClearError = onClearError
            )
            ValidatedOutlinedTextField(
                value = maxText,
                originalValue = options.max.toString(),
                onValueChange = { maxText = filterRangeInput(it).take(MAX_INPUT_LENGTH) },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_max_value)) },
                validationError = validationError,
                isErrorApplicable = {
                    validationError?.optionsUpdate is NumRangeGeneratorMaxUpdate
                },
                keyboardOptions = numberKeyboardOptions,
                onSave = { onUpdateOptions(NumRangeGeneratorMaxUpdate(maxText)) },
                onClearError = onClearError
            )
        }
        is AlphanumGeneratorOptions -> {
            var lengthText by rememberSaveable { mutableStateOf(options.length.toString()) }
            var symbols by rememberSaveable { mutableStateOf(options.customSymbols) }
            ValidatedOutlinedTextField(
                value = lengthText,
                originalValue = options.length.toString(),
                onValueChange = { lengthText = filterNumericInput(it).take(MAX_INPUT_LENGTH) },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_max_length)) },
                validationError = validationError,
                isErrorApplicable = {
                    validationError?.optionsUpdate is AlphanumGeneratorLengthUpdate
                },
                keyboardOptions = numberKeyboardOptions,
                onSave = { onUpdateOptions(AlphanumGeneratorLengthUpdate(lengthText)) },
                onClearError = onClearError
            )
            Spacer(modifier = Modifier.height(8.dp))
            ValidatedOutlinedTextField(
                value = symbols,
                originalValue = options.customSymbols,
                onValueChange = {
                    symbols = filterNoWhitespace(it).take(MAX_CUSTOM_CHARACTERS_LENGTH)
                },
                modifier = modifier,
                label = { Text(text = stringResource(R.string.et_custom_chars)) },
                validationError = validationError,
                isErrorApplicable = {
                    validationError?.optionsUpdate is AlphanumGeneratorCustomSymbolsUpdate
                },
                keyboardOptions = textKeyboardOptions,
                onSave = { onUpdateOptions(AlphanumGeneratorCustomSymbolsUpdate(symbols)) },
                onClearError = onClearError
            )
        }
    }
}
```
currently, if user had a value like `12` and enters a new value `012` and hits save, save button wont hide because of `value != originalValue` condition. can we somehow just update the current value once it's saved to make sure we will use the sanitized/converted one from ui state?