Skip to content

LayrzSelectionHandlePainter

Kenny Mochizuki Escalona edited this page Aug 21, 2026 · 2 revisions

LayrzSelectionHandlePainter

A custom painter that renders text selection handles as teardrop-shaped glyphs.

Metadata
Domain: Selection
Phase: M2 (Selection framework)
Primitive: CustomPainter
Status: Shipped in 0.0.12.


Overview

LayrzSelectionHandlePainter is a CustomPainter that renders the teardrop-shaped selection handles used to mark the start, end, or collapsed position (caret) of a text selection.

The teardrop is a circular bulge with a square corner in one quadrant. The corner's orientation (which direction it points) depends on the handle type and is controlled by Transform.rotate in LayrzTextSelectionControls.


API Reference

class LayrzSelectionHandlePainter extends CustomPainter {
  /// The fill color of the handle.
  ///
  /// Typically obtained from design tokens: [tokens.colors.primary].
  final Color color;

  const LayrzSelectionHandlePainter({
    required this.color,
  });

  @override
  void paint(Canvas canvas, Size size) {
    // Renders the teardrop shape
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => ...;
}

Parameters

  • color (Color, required) — The fill color of the handle. Typically tokens.colors.primary (brand color).

Teardrop Geometry

The handle is drawn as a teardrop: a circle combined with a square corner, producing a circular bulge with a pointed corner.

Unrotated Shape (Base Orientation)

Unrotated, the teardrop's square corner points to the top-left (NW) quadrant of its 22×22 bounding box:

┌───────────────┐
│ ●             │ ← corner at (0, 0)
│   ╱╱╱╱╱╱╱    │ (circular bulge)
│ ╱╱  circle ╱  │
│ ╱ containing  ╱ │
│ ╱   square   ╱  │
└───────────────┘

How It's Drawn

The painter combines:

  1. A circle (the bulge) centered at (11, 11) with radius 11
  2. A square (the corner) filling the top-left quadrant (0–11, 0–11)

The path's winding rule determines that the result is a teardrop: filled where both shapes overlap, creating a circular bulge with a square corner sticking out.

Rotation for Handle Types

The handle's corner orientation is set via Transform.rotate in LayrzTextSelectionControls.buildHandle. Transform.rotate uses clockwise rotation for positive angles in Flutter:

Handle Type Rotation Angle Corner Points Usage
left 90° CW π/2 NE (up-right) Selection start handle (left edge of selection)
right 0° (no rotation) 0 NW (up-left) Selection end handle (right edge of selection)
collapsed 45° CW π/4 N (up) Collapsed cursor (caret)

Critical detail: These angles are exact and empirically determined. Changing them will misalign the handles with the text selection endpoints.


Rendering

The painter fills the teardrop shape with a single Paint object using the specified color:

final paint = Paint()..color = color;
final path = Path()
  ..addOval(circle)
  ..addRect(point);
canvas.drawPath(path, paint);

No stroke, no border, no additional visual effects — just a solid-filled teardrop.


Sizing

The handle is always 22 × 22 logical pixels. This size is:

  • Small enough to not obstruct the text being selected
  • Large enough to be touch-friendly on mobile (recommended touch target ≥ 48×48, but the handle's tap region is enlarged via GestureDetector)
  • Consistent with Material design conventions for selection handles

The painter is called with size = Size(22, 22) and scales its drawing to fit.


Usage

LayrzSelectionHandlePainter is not directly instantiated in normal use. Instead, it is created internally by LayrzTextSelectionControls.buildHandle:

CustomPaint(
  size: Size(22, 22),
  painter: LayrzSelectionHandlePainter(
    color: tokens.colors.primary,  // Brand color
  ),
)

For custom selection implementations, you would create it like:

CustomPaint(
  size: const Size(22, 22),
  painter: LayrzSelectionHandlePainter(
    color: Colors.blue,  // Your chosen color
  ),
)

Interaction Region

The visual teardrop (22×22) is part of a larger interactive region:

  • Visual size: 22 × 22 logical pixels (the teardrop)
  • Hit region: Defined by SizedBox(width: 22, height: 22) with GestureDetector(behavior: HitTestBehavior.translucent), making the full bounding box tappable
  • Drag handling: Flutter's drag recognizers take over once the user taps and drags a handle to extend the selection

Performance

The painter is lightweight:

  • No complex calculations or iterations
  • Single path construction and fill operation
  • No animation or state changes during paint (all state is external via Transform)

Repainting is triggered only when the color changes, handled by shouldRepaint.


Technical Notes

  • Path winding rule: The combination of addOval and addRect on the same path uses the default even-odd winding rule, producing the teardrop shape where both shapes are filled
  • Coordinate system: The painter works in the local 22×22 coordinate space. Transform.rotate is applied outside the painter, rotating the entire rendered teardrop
  • Colorization: The handle color is typically the primary brand color for consistency, but any color can be supplied
  • No decoration: The handle is a pure shape with no shadow, border, or outline — styling is applied via Container and BoxDecoration at a higher level if needed

Related Components


Last updated: 2026-08-20
Related documents: LayrzTextSelectionControls, Selection (D50)

Clone this wiki locally