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,
})

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,
}) {
  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);

  // Find the lowest y that is strictly below the current line
  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) {
    // Use full document stop list for cross-node fallback so we are not
    // limited to the candidate subset passed by the caller.
    final docStops = allStops ?? stops_;
    if (_isInLastNode(root, current.fragmentId)) {
      final last = docStops.last;
      if (last == current) return NavigationResult.none;
      return NavigationResult(position: last, preferredX: x);
    }
    // No line below in this node → jump to the first stop of the next
    // top-level node that has caret stops (skip block nodes like HR).
    final currentNodeId = _findTopLevelNodeId(root, current.fragmentId);
    if (currentNodeId != null) {
      final currentNodeIdx = 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 (_nodeContainsFragment(nextNode, stop.fragmentId)) {
            return NavigationResult(position: stop, preferredX: x);
          }
        }
      }
    }
    // Nothing below → go to the end of the document
    final last = docStops.last;
    if (last == current) return NavigationResult.none;
    return NavigationResult(position: last, preferredX: x);
  }

  final lineStops = stops_
      .where((s) => (cachedY(s) - targetY!).abs() <= _kLineYTolerance)
      .toList();

  final best = _stopNearestX(lineStops, x, cachedX);
  return NavigationResult(position: best, preferredX: x);
}