Skip to content

Instantly share code, notes, and snippets.

@jezell
Last active February 23, 2025 07:52
Show Gist options
  • Save jezell/f1ae3365ec51f1be455abe8815663376 to your computer and use it in GitHub Desktop.
Save jezell/f1ae3365ec51f1be455abe8815663376 to your computer and use it in GitHub Desktop.

In Flutter, the “cursor” you see when typing into a TextField (or any other text input widget) is actually drawn (painted) by a lower-level rendering object called RenderEditable, which lives inside an EditableText widget. Here is a conceptual overview of how it works:


1. The hierarchy under the hood

  1. TextField:
    The high-level widget most developers use. It wraps lower-level widgets to handle text input, focus, styling, etc.

  2. EditableText:
    The core widget responsible for text editing, text layout, and keyboard interactions. When you create a TextField, Flutter internally creates an EditableText to handle editing logic.

  3. RenderEditable:
    The RenderObject for EditableText. This is where most of the painting (drawing) logic for text, selection highlights, and the cursor actually happens.


2. How the cursor is painted

Inside RenderEditable, there is a function (or set of functions) that handles drawing the cursor, often referred to in code as the caret:

  1. Calculating the caret (cursor) position

    • When a user taps or types, Flutter calculates where the next character goes using text layout metrics.
    • RenderEditable uses a text layout engine (TextPainter) to figure out the coordinates of the glyph positions.
    • Once it knows the exact offset within the text where the cursor should appear, it saves that position for painting.
  2. Blinking animation

    • By default, Flutter animates the cursor to blink (show and hide).
    • When the EditableText gains focus, a timer is set up to periodically toggle a boolean (e.g. _showCursor) on and off.
    • Every time _showCursor changes, RenderEditable is told to repaint.
  3. Painting the caret

    • On every frame, if _showCursor is true, the render object calls an internal method (something like _paintCaret) to draw the cursor.
    • This method uses a Canvas to paint a simple vertical line (or a rounded-rectangle if you set a cursorRadius) at the computed caret offset.
    • The width of that line is cursorWidth (usually 2.0 logical pixels by default), and the height generally matches the height of the current text line.
  4. Customization

    • cursorColor, cursorWidth, and cursorRadius can all be provided by EditableText (and in turn by TextField).
    • The blinking frequency is determined by cursorBlinkInterval (defaults to around 500ms).

3. Relevant code snippets (simplified)

While the actual Flutter engine code is quite extensive, a simplified flow might look like this:

class RenderEditable extends RenderBox {
  bool _showCursor = false;
  Offset _caretOffset;

  // Called by Flutter when we need to paint the text field:
  @override
  void paint(PaintingContext context, Offset offset) {
    // 1. Paint text content, selection highlights, etc.
    // 2. Paint the cursor if needed
    if (_showCursor) {
      _paintCaret(context.canvas, offset + _caretOffset);
    }
  }

  void _paintCaret(Canvas canvas, Offset caretOffset) {
    final Paint paint = Paint()..color = cursorColor;
    final double caretHeight = _computeCaretHeight();
    final Rect caretRect = Rect.fromLTWH(
      caretOffset.dx,
      caretOffset.dy,
      cursorWidth,
      caretHeight,
    );

    if (cursorRadius != null) {
      // Draw a rounded-rect caret
      final RRect caretRRect = RRect.fromRectAndRadius(caretRect, cursorRadius);
      canvas.drawRRect(caretRRect, paint);
    } else {
      // Draw a simple rectangle caret
      canvas.drawRect(caretRect, paint);
    }
  }
}

Then, somewhere in EditableTextState, a Ticker or periodic callback toggles _showCursor, causing repaints:

void _startCursorTimer() {
  _cursorTimer = Timer.periodic(_cursorBlinkInterval, (Timer timer) {
    setState(() {
      _showCursor = !_showCursor;
    });
  });
}

4. Putting it all together

  1. User focuses the TextField.
    Flutter starts the cursor blink timer in EditableTextState.
  2. RenderEditable is marked for repaint regularly (for blinking).
  3. When _showCursor is true, RenderEditable calls _paintCaret.
  4. A small vertical rectangle (or rounded rectangle) is drawn at the correct offset where new text will appear.
  5. Cursor blinks by toggling _showCursor on each timer tick, thus showing and hiding the caret.

Summary

The cursor in a Flutter TextField is essentially a blinking vertical line (or rectangle) painted by the RenderEditable object inside the EditableText widget. A timer toggles the cursor’s visibility, and the text layout system continuously updates its position based on user input and selection changes.

Key points:

  • It’s drawn as part of the normal paint cycle for the text editing widget (RenderEditable).
  • Its blinking is controlled by a timer that triggers a rebuild/repaint every half-second (by default).
  • You can customize its color, thickness, radius, and blink rate through the parameters on TextField or EditableText.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment