hitTest method

bool hitTest(
  1. BoxHitTestResult result, {
  2. required Offset position,
})

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;
}