getFragmentAtPosition method

({String fragmentId, int localOffset})? getFragmentAtPosition(
  1. Offset position
)

Implementation

({String fragmentId, int localOffset})? getFragmentAtPosition(
  Offset position,
) {
  // The TextPainter works in coordinates relative to the text (x=0).
  // Subtract the alignment offset to map the local position
  // of the render object to TextPainter coordinates.
  final adjustedPosition = position - Offset(_alignmentXOffset, 0);

  // FAST PATH: TextPainter.getPositionForOffset does a binary search
  // internally (O(log n)) and returns the closest text position. This
  // replaces the O(n) character-by-character loop that called
  // getBoxesForSelection once per character — a major bottleneck during
  // drag selection on long paragraphs (1000+ characters).
  final textPosition = _painter.getPositionForOffset(adjustedPosition);
  final result = _globalToLocal(textPosition.offset);
  if (result != null) return result;

  // FALLBACK: if getPositionForOffset returns an offset that does not
  // map to any fragment (should be rare), use line metrics to find the
  // nearest character on the target line.
  if (_lineMetricsDirty || _cachedLineMetrics == null) {
    _cachedLineMetrics = _painter.computeLineMetrics();
    _lineMetricsDirty = false;
  }
  final lineMetrics = _cachedLineMetrics!;
  if (lineMetrics.isEmpty) return null;

  int targetLine = lineMetrics.length - 1;
  for (int i = 0; i < lineMetrics.length; i++) {
    final line = lineMetrics[i];
    final lineTop = line.baseline - line.ascent;
    final lineBottom = line.baseline + line.descent;
    if (adjustedPosition.dy >= lineTop && adjustedPosition.dy <= lineBottom) {
      targetLine = i;
      break;
    }
    if (i == 0 && adjustedPosition.dy < lineTop) {
      targetLine = 0;
      break;
    }
  }

  final targetLineMetric = lineMetrics[targetLine];
  final targetLineY = targetLineMetric.baseline - targetLineMetric.ascent;
  final targetLineBottom =
      targetLineMetric.baseline + targetLineMetric.descent;

  String? bestFragmentId;
  int bestLocalOffset = 0;
  double bestXDistance = double.infinity;

  for (final fragment in _fragmentPositions) {
    for (int offset = 0; offset < fragment.textLength; offset++) {
      final globalOffset = fragment.start + offset;

      final boxes = _painter.getBoxesForSelection(
        TextSelection(
          baseOffset: globalOffset,
          extentOffset: globalOffset + 1,
        ),
      );
      if (boxes.isEmpty) continue;

      final box = boxes.first.toRect();
      final boxCenterY = (box.top + box.bottom) / 2;
      if (boxCenterY < targetLineY || boxCenterY > targetLineBottom) {
        continue;
      }

      if (box.contains(adjustedPosition)) {
        final isRightHalf = adjustedPosition.dx > (box.left + box.right) / 2;
        return (
          fragmentId: fragment.id,
          localOffset: isRightHalf ? offset + 1 : offset,
        );
      }

      final leftDistance = (adjustedPosition.dx - box.left).abs();
      final rightDistance = (adjustedPosition.dx - box.right).abs();

      if (leftDistance < bestXDistance) {
        bestXDistance = leftDistance;
        bestFragmentId = fragment.id;
        bestLocalOffset = offset;
      }
      if (rightDistance < bestXDistance) {
        bestXDistance = rightDistance;
        bestFragmentId = fragment.id;
        bestLocalOffset = offset + 1;
      }
    }
  }

  if (bestFragmentId != null) {
    return (fragmentId: bestFragmentId, localOffset: bestLocalOffset);
  }

  return null;
}