hitTest method
Hit test the inline content.
Implementation
bool hitTest(BoxHitTestResult result, {required Offset position}) {
// Paragraph path: hit test atomic inlines at parentData offsets (content-relative)
final double contentOriginX =
container.renderStyle.paddingLeft.computedValue + container.renderStyle.effectiveBorderLeftWidth.computedValue;
final double contentOriginY =
container.renderStyle.paddingTop.computedValue + container.renderStyle.effectiveBorderTopWidth.computedValue;
for (int i = 0; i < _allPlaceholders.length && i < _placeholderBoxes.length; i++) {
final ph = _allPlaceholders[i];
if (ph.kind != _PHKind.atomic) continue;
final rb = ph.atomic;
if (rb == null) continue;
// Use the direct child (wrapper) that carries the parentData offset
RenderBox hitBox = rb;
RenderObject? p = rb.parent;
while (p != null && p != container) {
if (p is RenderBox) hitBox = p;
p = p.parent;
}
final RenderLayoutParentData pd = hitBox.parentData as RenderLayoutParentData;
final Offset contentLocalOffset = Offset(pd.offset.dx - contentOriginX, pd.offset.dy - contentOriginY);
final bool isHit = result.addWithPaintOffset(
offset: contentLocalOffset,
position: position,
hitTest: (BoxHitTestResult res, Offset transformed) {
return hitBox.hitTest(res, position: transformed);
},
);
if (isHit) return true;
}
// Paragraph path: hit test non-atomic inline elements (e.g., <span>) using text ranges
if (_paragraph != null && _elementRanges.isNotEmpty) {
// Search from deepest descendants first to better match nested inline targets
final entries = _elementRanges.entries.toList()
..sort((a, b) => _depthFromContainer(b.key).compareTo(_depthFromContainer(a.key)));
for (final entry in entries) {
final RenderBoxModel box = entry.key;
final (int start, int end) = entry.value;
List<ui.TextBox> rects = const [];
bool synthesized = false;
if (end <= start) {
// Empty range: synthesize rects from extras for empty inline spans
rects = _synthesizeRectsForEmptySpan(box);
if (rects.isEmpty) continue;
synthesized = true;
} else {
rects = _paragraph!.getBoxesForRange(start, end);
if (rects.isEmpty) {
rects = _synthesizeRectsForEmptySpan(box);
if (rects.isEmpty) continue;
synthesized = true;
}
}
// Inflate rects to include padding and borders
final style = box.renderStyle;
final padL = style.paddingLeft.computedValue;
final padR = style.paddingRight.computedValue;
final padT = style.paddingTop.computedValue;
final padB = style.paddingBottom.computedValue;
final bL = style.effectiveBorderLeftWidth.computedValue;
final bR = style.effectiveBorderRightWidth.computedValue;
final bT = style.effectiveBorderTopWidth.computedValue;
final bB = style.effectiveBorderBottomWidth.computedValue;
for (int i = 0; i < rects.length; i++) {
final tb = rects[i];
double left = tb.left;
double right = tb.right;
double top = tb.top;
double bottom = tb.bottom;
final bool isFirst = (i == 0);
final bool isLast = (i == rects.length - 1);
// Horizontal expansion only on first/last fragments for real text fragments.
// For synthesized rects (built from extras), placeholders already include padding/border,
// so do not expand horizontally again to avoid oversizing the hit area.
if (!synthesized) {
if (isFirst) left -= (padL + bL);
if (isLast) right += (padR + bR);
}
// Vertical extent: include full content on every fragment. For synthesized spans,
// use effective line-height to match painted area; otherwise expand by padding/border.
if (synthesized) {
final double lineHeight = _effectiveLineHeightPx(style);
top = tb.top - (lineHeight + padT + bT);
bottom = tb.top + (padB + bB);
} else {
top -= (padT + bT);
bottom += (padB + bB);
}
if (position.dx >= left && position.dx <= right && position.dy >= top && position.dy <= bottom) {
// Prefer hitting the RenderEventListener wrapper if present, so events dispatch correctly
RenderEventListener? listener;
RenderObject? p = box;
while (p != null && p != container) {
if (p is RenderEventListener && p.enableEvent) {
listener = p;
break;
}
p = p.parent;
}
if (listener != null) {
// Convert container-local position to listener-local position
final Offset offsetToContainer = getLayoutTransformTo(listener, container);
final Offset local = position - offsetToContainer;
result.add(BoxHitTestEntry(listener, local));
return true;
} else {
// Fallback: add entry for the box itself
final Offset offsetToContainer = getLayoutTransformTo(box, container);
final Offset local = position - offsetToContainer;
result.add(BoxHitTestEntry(box, local));
return true;
}
}
}
}
}
return false;
}