moveUp function
NavigationResult
moveUp(
- Root root,
- CaretStop current,
- double preferredX,
- CaretXResolver resolveX,
- CaretYResolver resolveY, {
- List<
CaretStop> ? stops, - 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);
}