executeHandleArrowKey function

bool executeHandleArrowKey(
  1. LogicalKeyboardKey key,
  2. FluentDocument document, {
  3. bool ctrl = false,
  4. bool shift = false,
})

Implementation

bool executeHandleArrowKey(
  LogicalKeyboardKey key,
  FluentDocument document, {
  bool ctrl = false,
  bool shift = false,
}) {
  _ArrowKeyProfile.callCount++;
  final swTotal = Stopwatch()..start();

  final cursor = document.cursor;
  final current = shift
      ? CaretStop(cursor.focusId, cursor.focusOffset)
      : CaretStop(cursor.anchorId, cursor.anchorOffset);
  final root = document.content;

  // Measure caretStops getter (should be O(1) cached, but verify)
  final swStops = Stopwatch()..start();
  final stops = document.caretStops;
  swStops.stop();
  print('[ARROW_DETAIL] caretStops=${swStops.elapsedMicroseconds}μs len=${stops.length}');

  late final NavigationResult result;
  late final bool isVertical;

  final swWord = Stopwatch()..start();
  if (key == LogicalKeyboardKey.arrowLeft) {
    final swMove = Stopwatch()..start();
    result = ctrl
        ? moveWordLeft(root, current,
            stops: stops, cachedLines: document.logicalLines)
        : moveLeft(root, current, stops: stops);
    swMove.stop();
    print('[ARROW_DETAIL] moveLeft=${swMove.elapsedMicroseconds}μs');
    isVertical = false;
  } else if (key == LogicalKeyboardKey.arrowRight) {
    final swMove = Stopwatch()..start();
    result = ctrl
        ? moveWordRight(root, current,
            stops: stops, cachedLines: document.logicalLines)
        : moveRight(root, current, stops: stops);
    swMove.stop();
    print('[ARROW_DETAIL] moveRight=${swMove.elapsedMicroseconds}μs');
    isVertical = false;
  } else if (key == LogicalKeyboardKey.arrowUp ||
             key == LogicalKeyboardKey.arrowDown) {
    // Check if cursor is on a block-level node (HR, Image).
    // These nodes don't have reliable Y coordinates, so use index-based navigation.
    final currentNode = document.nodeById(current.fragmentId);
    final isBlockNode = currentNode is HorizontalRule || currentNode is FluentImage;

    if (isBlockNode) {
      final allStops = document.caretStops;
      final currentIdx = findStopIndex(allStops, current.fragmentId, current.offset);
      if (currentIdx >= 0) {
        if (key == LogicalKeyboardKey.arrowUp) {
          if (currentIdx > 0) {
            result = NavigationResult(position: allStops[currentIdx - 1], preferredX: -1.0);
          } else {
            result = NavigationResult.none;
          }
        } else { // arrowDown
          // Skip all stops of the current block node
          int nextIdx = currentIdx + 1;
          while (nextIdx < allStops.length &&
                 allStops[nextIdx].fragmentId == current.fragmentId) {
            nextIdx++;
          }
          if (nextIdx < allStops.length) {
            result = NavigationResult(position: allStops[nextIdx], preferredX: -1.0);
          } else {
            result = NavigationResult.none;
          }
        }
        isVertical = true;
      }
    } else {
      // Vertical navigation: scan only the current container + neighbours
      // instead of every stop in the document. Reduces O(n_stops) → O(container).
      final swVert = Stopwatch()..start();
      final currentContainerId = document.findLogicalContainerId(current.fragmentId);
      final containerOrder = document.containerOrder;
      final containerIdx = containerOrder.indexOf(currentContainerId ?? '');
      final candidateIds = <String>{};
      if (containerIdx >= 0) {
        candidateIds.add(containerOrder[containerIdx]);

        // If inside a table or list, expand to include ALL containers in that
        // structure so Up/Down can cross rows/items.  The immediate
        // predecessor / successor in containerOrder are only added if they
        // are OUTSIDE the structure, otherwise the structural expansion
        // already covers them and they can point to the wrong column/row.
        String? _nearestStructure(String? id) {
          if (id == null) return null;
          String? pid = id;
          while (pid != null) {
            final node = document.nodeById(pid);
            if (node is FluentTable || node is FluentList) return pid;
            pid = document.findParentCached(pid);
          }
          return null;
        }

        bool _isInsideStructure(String? containerId, String structureId) {
          if (containerId == null) return false;
          String? pid = containerId;
          while (pid != null) {
            if (pid == structureId) return true;
            pid = document.findParentCached(pid);
          }
          return false;
        }

        String? _cellFor(String? containerId) {
          if (containerId == null) return null;
          String? pid = containerId;
          while (pid != null) {
            final node = document.nodeById(pid);
            if (node is FluentCell) return pid;
            pid = document.findParentCached(pid);
          }
          return null;
        }

        final currentEnclosing = _nearestStructure(currentContainerId);
        if (currentEnclosing != null) {
          final enclosingNode = document.nodeById(currentEnclosing);
          if (enclosingNode is FluentTable) {
            final table = enclosingNode;
            final currentCellId = _cellFor(currentContainerId);
            if (currentCellId != null) {
              int rowIndex = -1;
              int colIndex = -1;
              for (int r = 0; r < table.rows.length; r++) {
                final row = table.rows[r];
                for (int c = 0; c < row.cells.length; c++) {
                  if (row.cells[c].id == currentCellId) {
                    rowIndex = r;
                    colIndex = c;
                    break;
                  }
                }
                if (rowIndex >= 0) break;
              }
              // Current cell + cell above + cell below (same column only)
              if (rowIndex >= 0 && colIndex >= 0) {
                for (final id in containerOrder) {
                  if (_isInsideStructure(id, currentCellId)) {
                    candidateIds.add(id);
                  }
                }
                if (rowIndex > 0 && colIndex < table.rows[rowIndex - 1].cells.length) {
                  final aboveCellId = table.rows[rowIndex - 1].cells[colIndex].id;
                  for (final id in containerOrder) {
                    if (_isInsideStructure(id, aboveCellId)) {
                      candidateIds.add(id);
                    }
                  }
                }
                if (rowIndex < table.rows.length - 1 &&
                    colIndex < table.rows[rowIndex + 1].cells.length) {
                  final belowCellId = table.rows[rowIndex + 1].cells[colIndex].id;
                  for (final id in containerOrder) {
                    if (_isInsideStructure(id, belowCellId)) {
                      candidateIds.add(id);
                    }
                  }
                }
              }
            }
          } else if (enclosingNode is FluentList) {
            // Lists are 1-D: full structural expansion
            for (final id in containerOrder) {
              if (_isInsideStructure(id, currentEnclosing)) {
                candidateIds.add(id);
              }
            }
          }
          // Find first predecessor OUTSIDE the structure (exit upward)
          if (containerIdx > 0) {
            for (int i = containerIdx - 1; i >= 0; i--) {
              final id = containerOrder[i];
              if (!_isInsideStructure(id, currentEnclosing)) {
                candidateIds.add(id);
                break;
              }
            }
          }
          // Find first successor OUTSIDE the structure (exit downward)
          if (containerIdx < containerOrder.length - 1) {
            for (int i = containerIdx + 1; i < containerOrder.length; i++) {
              final id = containerOrder[i];
              if (!_isInsideStructure(id, currentEnclosing)) {
                candidateIds.add(id);
                break;
              }
            }
          }
        } else {
          // Not inside a table/list: use immediate neighbours as before.
          if (containerIdx > 0) candidateIds.add(containerOrder[containerIdx - 1]);
          if (containerIdx < containerOrder.length - 1) {
            candidateIds.add(containerOrder[containerIdx + 1]);
          }
        }
      }
      final candidateStops = candidateIds.isNotEmpty
          ? candidateIds
              .expand<CaretStop>((id) => document.stopsByContainer[id] ?? [])
              .toList()
          : stops;
      final swMove = Stopwatch()..start();
      final pref = _adjustPreferredXForBlockImage(document, current, cursor.preferredX);
      if (key == LogicalKeyboardKey.arrowUp) {
        result = moveUp(root, current, pref,
            document.resolveCaretX, document.resolveCaretY, stops: candidateStops);
      } else {
        result = moveDown(root, current, pref,
            document.resolveCaretX, document.resolveCaretY, stops: candidateStops);
      }
      swMove.stop();
      swVert.stop();
      print('[ARROW_DETAIL] vertSetup=${swVert.elapsedMicroseconds}μs move=${swMove.elapsedMicroseconds}μs');
      isVertical = true;
    }
  } else {
    return false;
  }
  swWord.stop();
  _ArrowKeyProfile.totalWordNavUs += swWord.elapsedMicroseconds;
  if (swWord.elapsedMicroseconds > _ArrowKeyProfile.maxWordNavUs) {
    _ArrowKeyProfile.maxWordNavUs = swWord.elapsedMicroseconds;
  }

  final newPos = result.position;
  if (newPos == null) return true;

  if (shift) {
    cursor.batchUpdate(() {
      cursor.focusTo(newPos.fragmentId, newPos.offset);
      cursor.preferredX = isVertical ? result.preferredX : -1.0;
    });
  } else {
    cursor.batchUpdate(() {
      cursor.moveTo(newPos.fragmentId, newPos.offset);
      if (isVertical) cursor.preferredX = result.preferredX;
    });
    document.syncPendingFontWithCursor();
  }

  // Keep SelectionManager always in sync with cursor anchor/focus
  final swSync = Stopwatch()..start();
  _syncSelectionManager(document);
  swSync.stop();
  _ArrowKeyProfile.totalSyncSelUs += swSync.elapsedMicroseconds;
  if (swSync.elapsedMicroseconds > _ArrowKeyProfile.maxSyncSelUs) {
    _ArrowKeyProfile.maxSyncSelUs = swSync.elapsedMicroseconds;
  }

  // Arrow navigation never mutates content: use the cursor-only notification
  // so the cached caret-stop rail and node index survive across key presses.
  final swCursor = Stopwatch()..start();
  document.cursorOnlyUpdate();
  swCursor.stop();
  _ArrowKeyProfile.totalCursorUpUs += swCursor.elapsedMicroseconds;

  swTotal.stop();
  _ArrowKeyProfile.totalTimeUs += swTotal.elapsedMicroseconds;
  if (swTotal.elapsedMicroseconds > _ArrowKeyProfile.maxTotalUs) {
    _ArrowKeyProfile.maxTotalUs = swTotal.elapsedMicroseconds;
  }
  _ArrowKeyProfile._maybeReport();

  return true;
}