findRowAtPaintedY method

({double extent, TKey key, double paintedOffset})? findRowAtPaintedY(
  1. double scrollY
)

Finds the first live (non-pending-deletion) visible row whose painted scroll-space range [paintedOffset, paintedOffset + extent) contains scrollY, falling back to the last live row when scrollY sits past the bottom of the tree. Returns null when the visible order is empty or every entry is pending-deletion.

Painted offsets include the node's current FLIP slide delta, matching what snapshotVisibleOffsets would return — but without allocating an O(N) map. Designed for TreeReorderController, which polls the hovered row every pointer move and every autoscroll tick; the previous implementation materialized a Map<TKey, double> for the whole tree on each call.

Fast path (no active slides): O(log N) via binary search on structural offsets, plus a forward scan to skip pending-deletion rows.

Slow path (active slides): O(N) linear scan — slide deltas can reorder painted positions relative to structural positions, so binary search over structural offsets is unsafe. A slide only overlaps a drag when the user starts a new drag while a prior commit's FLIP is still animating (≤ slideDuration).

Implementation

({TKey key, double paintedOffset, double extent})? findRowAtPaintedY(
  double scrollY,
) {
  final visible = controller.visibleNodes;
  if (visible.isEmpty) return null;

  if (controller.hasActiveSlides) {
    TKey? lastLiveKey;
    double lastLiveOffset = 0.0;
    double lastLiveExtent = 0.0;
    double structural = 0.0;
    final orderNids = controller.orderNidsView;
    final edgeExits = _phantomEdgeExits;
    // Lazy: only build viewport snapshot if there are ghosts to
    // resolve. Edge ghosts paint at the LIVE viewport edge.
    final _ViewportSnapshot? viewportForGhosts =
        edgeExits != null ? _currentViewportSnapshot() : null;
    for (int i = 0; i < visible.length; i++) {
      final nid = orderNids[i];
      final key = visible[i];
      final extent = controller.getCurrentExtentNid(nid);
      final slide = controller.getSlideDeltaNid(nid);
      // Edge-ghost rows paint at `_edgeGhostBaseY + slide`, not at
      // `structural + slide`. Substitute so drag-target lookup lands
      // on the correct ghost row.
      final edgeEntry = edgeExits?[key];
      final paintedOffset = edgeEntry != null
          ? _edgeGhostBaseY(edgeEntry, viewportForGhosts!) + slide
          : structural + slide;
      if (!controller.isPendingDeletion(key)) {
        if (scrollY < paintedOffset + extent) {
          return (key: key, paintedOffset: paintedOffset, extent: extent);
        }
        lastLiveKey = key;
        lastLiveOffset = paintedOffset;
        lastLiveExtent = extent;
      }
      structural += extent;
    }
    if (lastLiveKey == null) return null;
    return (
      key: lastLiveKey,
      paintedOffset: lastLiveOffset,
      extent: lastLiveExtent,
    );
  }

  // Fast path: no slides active, painted offset == structural offset.
  final startIdx = _findFirstVisibleIndex(scrollY);
  for (int i = startIdx; i < visible.length; i++) {
    final key = visible[i];
    if (controller.isPendingDeletion(key)) continue;
    return _liveRowAt(i, key);
  }
  // Past the end (or every trailing row is pending-deletion) — walk back
  // for the last live row.
  for (int i = visible.length - 1; i >= 0; i--) {
    final key = visible[i];
    if (controller.isPendingDeletion(key)) continue;
    return _liveRowAt(i, key);
  }
  return null;
}