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