getFragmentAtPosition method
({String fragmentId, int localOffset})?
getFragmentAtPosition(
- 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;
}