formatEditUpdate method

  1. @override
TextEditingValue formatEditUpdate(
  1. TextEditingValue oldValue,
  2. TextEditingValue newValue
)
override

Formats text input by filtering digits and managing cursor position

Called automatically by Flutter's text input system when user types.

Process:

  1. Extract only digits from new input
  2. Limit to 4 digits maximum
  3. Calculate appropriate cursor position
  4. 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),
  );
}