insertionIndexAt method
returns the index at which an object being inserted at position p (where p is relative to the top left of the wrap) usually used for drag and drop insertion index calculation insertionSpacingForClear, used in cases where the insertion indicator should be placed in a clear zone, not right adjacent to a child (eg, if an insertion is to be done after the end of a row, where the row isn't close to the edge of the wrap), is the amount of space to put between the center of the insertion point and the edge of the container or the nearest child.
Implementation
InsertionPoint insertionIndexAt(Offset p,
{double insertionSpacingForClear = 20}) {
// if not null, then it's updated by performLayout
if (previousBoxConstraints == null) {
// this would never happen, since it would mean the drag and drop is happening before the container has been laid out, which a user can't do
// note, the position isn't quite right, but this might not get used
return const InsertionPoint(
index: 0,
position: Offset.zero,
insertingAfter: false,
inserterWide: true);
}
// we compute from previous frame's positions
previousComputedRuns ??=
_computeRuns(previousBoxConstraints!, ChildLayoutHelper.dryLayoutChild);
final (AxisSize childrenAxisSize, List<_RunMetrics> runMetrics) =
previousComputedRuns!;
// we generally work with normalized positions where runs go to the right and down, we flip back at the end
// normalized size: width is the main axis extent, height is the cross axis extent
final Size rsize = direction == Axis.horizontal ? size : flipSize(size);
// In the normalized frame the main axis is x and the cross axis is y, so the
// main axis is flipped by flipMainAxis and the cross axis by flipCrossAxis.
final (bool flipMainAxis, bool flipCrossAxis) = _areAxesFlipped;
double maybeFlippedAxis(double x, bool flipped, double span) =>
flipped ? span - x : x;
Offset transform(Offset o) {
Offset result = o;
result = direction == Axis.horizontal ? result : flipOffset(result);
result = Offset(
maybeFlippedAxis(result.dx, flipMainAxis, rsize.width),
maybeFlippedAxis(result.dy, flipCrossAxis, rsize.height));
return result;
}
Offset transformBack(Offset o) {
// reverse of normalizeRect for offset
Offset result = o;
result = Offset(
maybeFlippedAxis(result.dx, flipMainAxis, rsize.width),
maybeFlippedAxis(result.dy, flipCrossAxis, rsize.height));
result = direction == Axis.horizontal ? result : flipOffset(result);
return result;
}
// translates a rect representing the position of a child so that it is as if the flow is left to right top to bottom
Rect normalizeRect(Rect rect) {
Rect r = rect;
r = direction == Axis.horizontal
? r
: flipOffset(r.topLeft) & flipSize(r.size);
r = !flipMainAxis
? r
: Offset(rsize.width - (r.topLeft.dx + r.width), r.topLeft.dy) &
r.size;
r = !flipCrossAxis
? r
: Offset(r.topLeft.dx, rsize.height - (r.topLeft.dy + r.height)) &
r.size;
return r;
}
final Offset pr = transform(p);
// performance opportunity if needed; you can speed this up by first doing a spine check, where you check the leadingChild of each row to rule most rows out
// complications: we don't know which row we're in until we've processed the following row to find its lowest y and see whether it's closer to the insertion point than the prev row's highest y
// remember the nearest one from the previous row, check the next row to see if there's anything nearer per the y of that row, if not, it's the one from the previous.
if (runMetrics.isEmpty) {
return InsertionPoint(
index: 0,
insertingAfter: false,
position: Offset(insertionSpacingForClear, insertionSpacingForClear),
inserterWide: true);
}
int itotal = 0;
// whether the cursor should be rendered on one of the ends
bool prevNearestIndicatorIsWide = true;
// these would seem to be the opposite of their names, this is because the coordinate system flips the y axis
double prevRowsLowestY = double.negativeInfinity;
double prevRowsHighestY = double.infinity;
double prevRowsNearestIndicatorX = 0;
bool prevNearestAfter = false;
int prevRowsNearestChildIndex = 0;
for (int i = 0; i < runMetrics.length; i++) {
final _RunMetrics run = runMetrics[i];
if (run.childCount == 0) {
continue;
}
double thisRowsLowestY = double.negativeInfinity;
double thisRowsHighestY = double.infinity;
// (nearest in x, nearest from the edge of the child)
double nearestChildDistanceX = double.infinity;
int nearestChildIndex = 0;
double? nearestIndicatorX;
// whether the indicator is right next to the child/between the child and the edge of the widget
bool nearestIndicatorIsWide = false;
RenderBox? child = run.leadingChild;
double? previousChildRightEdge;
bool nearestAfter = false;
bool nearestIndicatorXNeedsSetting = false;
for (int j = 0; j < run.childCount; j++) {
if (child == null) {
break;
}
final Rect childBounds = normalizeRect(
(child.parentData as InsertableWrapParentData).offset & child.size);
if (childBounds.bottom > thisRowsLowestY) {
thisRowsLowestY = childBounds.bottom;
}
if (childBounds.top < thisRowsHighestY) {
thisRowsHighestY = childBounds.top;
}
if (nearestIndicatorXNeedsSetting) {
nearestIndicatorX = (previousChildRightEdge! + childBounds.left) / 2;
nearestIndicatorXNeedsSetting = false;
}
bool insertingAfter = pr.dx > childBounds.center.dx;
bool isInside = pr.dx >= childBounds.left && pr.dx < childBounds.right;
// check nearness to each side of the child
double leftDistance = (childBounds.left - pr.dx).abs();
// when there's no gap between two items, they have the same edge, in this case we make sure the point is considered to be closer to the edge belonging to the item that it's inside
if (leftDistance < nearestChildDistanceX ||
(isInside && leftDistance <= nearestChildDistanceX)) {
nearestChildIndex = itotal;
nearestAfter = insertingAfter;
nearestChildDistanceX = leftDistance;
if (previousChildRightEdge != null) {
nearestIndicatorIsWide = false;
nearestIndicatorX = childBounds.left - insertionSpacingForClear;
} else {
nearestIndicatorIsWide = true;
nearestIndicatorX = previousChildRightEdge == null
? childBounds.left
: (previousChildRightEdge + childBounds.left) / 2;
}
}
double rightDistance = (childBounds.right - pr.dx).abs();
if (rightDistance < nearestChildDistanceX) {
nearestChildIndex = itotal;
nearestAfter = insertingAfter;
nearestChildDistanceX = rightDistance;
// we don't know the next child's left, so can't set nearestX, this flag will make sure it's done either way
nearestIndicatorXNeedsSetting = true;
}
itotal += 1;
previousChildRightEdge = childBounds.right;
child = (child.parentData as InsertableWrapParentData).nextSibling;
}
if (nearestIndicatorXNeedsSetting) {
nearestIndicatorX = previousChildRightEdge! + insertionSpacingForClear;
}
if (pr.dy > (thisRowsHighestY + prevRowsLowestY) / 2 ||
// if prevLowestY is unset like so, it means this is the first row
prevRowsLowestY == double.negativeInfinity) {
//then it could be in this row
if (pr.dy < thisRowsLowestY) {
// then it is in this row. Otherwise, continue on to the next row to find out where that boundary is, and then we'll know for sure
return InsertionPoint(
index: nearestChildIndex,
insertingAfter: nearestAfter,
position: transformBack(Offset(nearestIndicatorX!,
(thisRowsLowestY + thisRowsHighestY) / 2)),
inserterWide: nearestIndicatorIsWide);
}
} else {
//then it's actually in the previous row
return InsertionPoint(
index: prevRowsNearestChildIndex,
insertingAfter: prevNearestAfter,
position: transformBack(Offset(prevRowsNearestIndicatorX,
(prevRowsLowestY + prevRowsHighestY) / 2)),
inserterWide: prevNearestIndicatorIsWide);
}
prevRowsLowestY = thisRowsLowestY;
prevRowsHighestY = thisRowsHighestY;
prevRowsNearestIndicatorX = nearestIndicatorX ?? 0;
prevRowsNearestChildIndex = nearestChildIndex;
prevNearestAfter = nearestAfter;
prevNearestIndicatorIsWide = nearestIndicatorIsWide;
}
return InsertionPoint(
// after the last child
index: max(itotal - 1, 0),
insertingAfter: true,
position: transformBack(
Offset(insertionSpacingForClear, insertionSpacingForClear)),
inserterWide: true);
}