formatEditUpdate method
Formats text input by filtering digits and managing cursor position
Called automatically by Flutter's text input system when user types.
Process:
- Extract only digits from new input
- Limit to 4 digits maximum
- Calculate appropriate cursor position
- Return formatted TextEditingValue
Overwrite Mode (HH:MM): When all 4 digit slots are filled and the user types a new digit, the digit at the current cursor position is replaced rather than inserted. This gives a fixed-format masked-input feel: each keystroke overwrites the digit under (or just before) the cursor and advances to the next slot.
Backspace:
Clears the targeted digit to 0 (when 4 digits present) or removes it.
The cursor correctly tracks across the : separator into hour digits.
oldValue - Previous TextEditingValue
newValue - New TextEditingValue from user input
Returns filtered and formatted TextEditingValue
Implementation
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final oldDigits = _limitToFourDigits(
TimeInputControllers._removeNonDigits(oldValue.text));
final newRawDigits = TimeInputControllers._removeNonDigits(newValue.text);
// ── BACKSPACE ──────────────────────────────────────────────────────────
bool isBackspace = false;
int backspaceSlot = 0;
if (_isCollapsedBackspace(oldValue, newValue)) {
final removalIndex = _countDigitsBeforeOffset(
oldValue.text, oldValue.selection.baseOffset) -
1;
if (removalIndex >= 0 && removalIndex < oldDigits.length) {
isBackspace = true;
backspaceSlot = removalIndex;
}
}
String limitedDigits;
int cursorSlot;
if (isBackspace) {
// Zero out the digit at the backspace slot (or remove if <4 digits).
if (oldDigits.length >= 4) {
limitedDigits = _replaceDigitAt(oldDigits, backspaceSlot, '0');
} else {
limitedDigits = _removeDigitAt(oldDigits, backspaceSlot);
}
cursorSlot = backspaceSlot;
} else if (newRawDigits.length > oldDigits.length &&
oldDigits.length >= 4) {
// ── OVERWRITE MODE ──────────────────────────────────────────────────
// All 4 digit slots are filled and the user typed a digit. Find the
// slot where the cursor was and replace that digit with the new one.
final cursorDigitBefore = _countDigitsBeforeOffset(
newValue.text, newValue.selection.baseOffset);
// The new value has one extra digit inserted; the cursor sits after it.
// Determine which digit slot the cursor was pointing AT before typing.
final slotToReplace =
(cursorDigitBefore - 1).clamp(0, oldDigits.length - 1);
// Find the newly inserted digit by locating the first position where
// the raw digit strings diverge.
int firstDiffPos = 0;
while (firstDiffPos < oldDigits.length &&
firstDiffPos < newRawDigits.length &&
newRawDigits[firstDiffPos] == oldDigits[firstDiffPos]) {
firstDiffPos++;
}
final newDigit =
firstDiffPos < newRawDigits.length ? newRawDigits[firstDiffPos] : '0';
limitedDigits = _replaceDigitAt(oldDigits, slotToReplace, newDigit);
// Advance cursor to the next digit slot after replacement.
cursorSlot = (slotToReplace + 1).clamp(0, oldDigits.length);
if (enableDebugLogs) {
assert(() {
debugPrint(
'[TI:formatter] OVERWRITE '
'old="${oldValue.text}" '
'new="${newValue.text}" '
'-> oldDigits="$oldDigits" newRaw="$newRawDigits" '
'slot=$slotToReplace newDigit=$newDigit '
'-> result="$limitedDigits" cursorSlot=$cursorSlot',
);
return true;
}());
}
} else {
// ── NORMAL INSERT ───────────────────────────────────────────────────
// Fewer than 4 digits or digit count hasn't grown – treat as insertion.
limitedDigits = _limitToFourDigits(newRawDigits);
final digitsBeforeCursor = _countDigitsBeforeOffset(
newValue.text, newValue.selection.baseOffset);
if (digitsBeforeCursor > 0 && limitedDigits.isNotEmpty) {
cursorSlot = digitsBeforeCursor.clamp(1, limitedDigits.length);
} else {
cursorSlot = 0;
}
if (enableDebugLogs) {
assert(() {
debugPrint(
'[TI:formatter] INSERT '
'new="${newValue.text}" sel=${newValue.selection.baseOffset} '
'-> digits="$limitedDigits" digitsBefore=$digitsBeforeCursor '
'-> cursorSlot=$cursorSlot',
);
return true;
}());
}
}
// Always display in formatted mode.
final formattedText = TimeInputControllers.formatTimeInput(
limitedDigits,
isUtc: isUtc,
showLocalIndicator: showLocalIndicator,
);
// Map the 0-based digit slot index to a formatted-text offset.
final newCursorPosition = _positionAtDigitSlot(formattedText, cursorSlot);
return TextEditingValue(
text: formattedText,
selection: TextSelection.collapsed(offset: newCursorPosition),
);
}