placeTooltip function

Offset placeTooltip({
  1. required Rect screen,
  2. required Rect hole,
  3. required TooltipPosition position,
  4. required Size size,
  5. required EdgeInsets safeArea,
  6. required List<Rect> avoid,
  7. double gap = _kGap,
})

Pure placement core, shared by the single and multi delegates: the offset for a tooltip of size around hole, picking the best fitting side from position's order (preferred → mirrored → orthogonal; auto — by free space) and vetoing placements that overlap avoid or leave the safe rect.

Anchor off the safe rect (the target is off screen — autoScroll is still scrolling it in, or the page was scrolled away): no side can fit, so the preferred side's offset is clamped into the safe rect instead — the tooltip stays anchored to the direction the target comes from (never a screen corner that has nothing to do with it) and joins the side placement continuously as the target arrives.

avoid must include the tooltip's own anchor hole — a tooltip never covers a spotlighted target. For multi-content slots the callers add the already-placed tooltip rects, so slots never overlap each other.

Implementation

Offset placeTooltip({
  required Rect screen,
  required Rect hole,
  required TooltipPosition position,
  required Size size,
  required EdgeInsets safeArea,
  required List<Rect> avoid,
  double gap = _kGap,
}) {
  final safe = _safeRectOf(screen, safeArea);
  for (final side in _sideOrder(position, safe, hole)) {
    final offset = _offsetFor(side, size, hole, safe, gap);
    if (_fits(offset, size, safe, avoid)) return offset;
  }
  // No side fit and the anchor itself is off the safe rect — autoScroll is
  // still bringing the target in, or the page was scrolled away from it. The
  // corner chain below would park the tooltip in a screen corner that has no
  // relation to the target (the "top-left flash" with a per-step
  // `autoScroll`). Stay on the target's preferred side instead, clamped into
  // the safe rect: the tooltip waits at the screen edge the target is coming
  // from and slides onto it, and the clamp joins the side placement above
  // continuously on the way in.
  if (!hole.overlaps(safe)) {
    for (final side in _sideOrder(position, safe, hole)) {
      final clamped =
          _clampIntoSafe(_offsetFor(side, size, hole, safe, gap), size, safe);
      // `avoid` still wins: a multi slot gives up its preferred side rather
      // than overlapping an already-placed tooltip.
      if (_fits(clamped, size, safe, avoid)) return clamped;
    }
  }

  // No side fit while the anchor IS on screen (tooltip/hole larger than the
  // safe rect, or every side is blocked) — try the safe-rect corners with a
  // margin (a multi slot does not land on an already-placed tooltip when a
  // corner is free); the last resort is the top-left corner (may still
  // overflow the safe rect bottom/right — nothing fits; the margin keeps it
  // as close as the degenerate case allows).
  final corners = [
    Offset(safe.left + _fallbackPadding, safe.top + _fallbackPadding),
    Offset(
      safe.right - _fallbackPadding - size.width,
      safe.top + _fallbackPadding,
    ),
    Offset(
      safe.left + _fallbackPadding,
      safe.bottom - _fallbackPadding - size.height,
    ),
    Offset(
      safe.right - _fallbackPadding - size.width,
      safe.bottom - _fallbackPadding - size.height,
    ),
  ];
  for (final corner in corners) {
    if (_fits(corner, size, safe, avoid)) return corner;
  }
  return corners.first;
}