moveDown function

NavigationResult moveDown(
  1. Root root,
  2. CaretStop current,
  3. double preferredX,
  4. CaretXResolver resolveX,
  5. CaretYResolver resolveY, {
  6. List<CaretStop>? stops,
  7. List<CaretStop>? allStops,
  8. ParentResolver? parentResolver,
  9. ContainerResolver? containerResolver,
  10. TopLevelIndexResolver? topLevelIndexResolver,
})

Moves the cursor down by one LogicalLine. allStops is the full document stop list used for cross-node fallback.

Implementation

NavigationResult moveDown(
  Root root,
  CaretStop current,
  double preferredX,
  CaretXResolver resolveX,
  CaretYResolver resolveY, {
  List<CaretStop>? stops,
  List<CaretStop>? allStops,
  ParentResolver? parentResolver,
  ContainerResolver? containerResolver,
  TopLevelIndexResolver? topLevelIndexResolver,
}) {
  final stops_ = stops ?? buildAllStops(root);
  if (stops_.isEmpty) return NavigationResult.none;

  final yCache = <CaretStop, double>{};
  double cachedY(CaretStop s) => yCache[s] ??= resolveY(s);
  final xCache = <CaretStop, double>{};
  double cachedX(CaretStop s) => xCache[s] ??= resolveX(s);

  final x = preferredX >= 0.0 ? preferredX : cachedX(current);
  final currentY = cachedY(current);

  double? targetY;
  for (final stop in stops_) {
    final y = cachedY(stop);
    if (y > currentY + _kLineYTolerance) {
      if (targetY == null || y < targetY) targetY = y;
    }
  }

  if (targetY == null) {
    final docStops = allStops ?? stops_;
    if (_isInLastNode(root, current.fragmentId,
        parentResolver: parentResolver, containerResolver: containerResolver,
        topLevelIndexResolver: topLevelIndexResolver)) {
      final last = docStops.last;
      if (last == current) return NavigationResult.none;
      return NavigationResult(position: last, preferredX: x);
    }
    final currentNodeId = _findTopLevelNodeId(root, current.fragmentId,
        parentResolver: parentResolver, containerResolver: containerResolver,
        topLevelIndexResolver: topLevelIndexResolver);
    if (currentNodeId != null) {
      final currentNodeIdx = topLevelIndexResolver != null
          ? topLevelIndexResolver(currentNodeId)
          : root.nodes.indexWhere((n) => n.id == currentNodeId);
      for (int i = currentNodeIdx + 1; i < root.nodes.length; i++) {
        final nextNode = root.nodes[i];
        for (final stop in docStops) {
          if (_stopBelongsToNode(stop, nextNode, root, containerResolver: containerResolver, parentResolver: parentResolver, topLevelIndexResolver: topLevelIndexResolver)) {
            return NavigationResult(position: stop, preferredX: x);
          }
        }
      }
    }
    final last = docStops.last;
    if (last == current) return NavigationResult.none;
    return NavigationResult(position: last, preferredX: x);
  }

  // Single-pass: find nearest-X stop on the target line without allocating.
  CaretStop? best;
  double bestDist = double.infinity;
  for (final stop in stops_) {
    if ((cachedY(stop) - targetY).abs() <= _kLineYTolerance) {
      final dist = (cachedX(stop) - x).abs();
      if (dist < bestDist) {
        bestDist = dist;
        best = stop;
      }
    }
  }
  if (best == null) return NavigationResult(position: current, preferredX: x);
  return NavigationResult(position: best, preferredX: x);
}