moveUp function

NavigationResult moveUp(
  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 up by one LogicalLine. preferredX is the x coordinate in pixels to maintain. If it's -1.0, it's calculated from the current position via resolveX. allStops is the full document stop list used for cross-node fallback.

Implementation

NavigationResult moveUp(
  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;

  // resolveY/resolveX are expensive (O(visible) each). Cache their
  // results for the duration of this call to avoid redundant lookups.
  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 highest y that is strictly above 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 (_isInFirstNode(root, current.fragmentId)) {
      final first = docStops.first;
      if (first == current) return NavigationResult.none;
      return NavigationResult(position: first, preferredX: x);
    }
    // No line above in this node → jump to the last stop of the previous
    // 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 >= 0; i--) {
        final prevNode = root.nodes[i];
        for (int j = docStops.length - 1; j >= 0; j--) {
          if (_nodeContainsFragment(prevNode, docStops[j].fragmentId)) {
            return NavigationResult(position: docStops[j], preferredX: x);
          }
        }
      }
    }
    // Nothing above → go to the start of the document
    final first = docStops.first;
    if (first == current) return NavigationResult.none;
    return NavigationResult(position: first, preferredX: x);
  }

  // Among the stops on the target line, take the one with closest x
  final lineStops = stops_
      .where((s) => (cachedY(s) - targetY!).abs() <= _kLineYTolerance)
      .toList();

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