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,
}) {
  final cursor = document.cursor;
  final current = shift
      ? CaretStop(cursor.focusId, cursor.focusOffset)
      : CaretStop(cursor.anchorId, cursor.anchorOffset);
  final root = document.content;

  final stops = document.caretStops;

  late final NavigationResult result;
  late final bool isVertical;

  // When there's an active selection and shift is not pressed, arrow keys
  // should collapse the selection to the appropriate edge rather than
  // moving from the anchor position.
  if (!shift && !cursor.isCollapsed) {
    final anchorIdx = findStopIndex(stops, cursor.anchorId, cursor.anchorOffset);
    final focusIdx = findStopIndex(stops, cursor.focusId, cursor.focusOffset);

    if (anchorIdx >= 0 && focusIdx >= 0) {
      final start = anchorIdx <= focusIdx
          ? CaretStop(cursor.anchorId, cursor.anchorOffset)
          : CaretStop(cursor.focusId, cursor.focusOffset);
      final end = anchorIdx <= focusIdx
          ? CaretStop(cursor.focusId, cursor.focusOffset)
          : CaretStop(cursor.anchorId, cursor.anchorOffset);

      if (key == LogicalKeyboardKey.arrowLeft ||
          key == LogicalKeyboardKey.arrowUp) {
        cursor.batchUpdate(() {
          cursor.moveTo(start.fragmentId, start.offset);
        });
        document.syncPendingFontWithCursor();
        document.selectionManager.collapse();
        _syncSelectionManager(document);
        document.cursorOnlyUpdate();
        return true;
      }
      if (key == LogicalKeyboardKey.arrowRight ||
          key == LogicalKeyboardKey.arrowDown) {
        cursor.batchUpdate(() {
          cursor.moveTo(end.fragmentId, end.offset);
        });
        document.syncPendingFontWithCursor();
        document.selectionManager.collapse();
        _syncSelectionManager(document);
        document.cursorOnlyUpdate();
        return true;
      }
    }
  }

  if (key == LogicalKeyboardKey.arrowLeft) {
    result = ctrl
        ? moveWordLeft(root, current,
            stops: stops, cachedLines: document.logicalLines)
        : moveLeft(root, current, stops: stops);
    isVertical = false;
  } else if (key == LogicalKeyboardKey.arrowRight) {
    result = ctrl
        ? moveWordRight(root, current,
            stops: stops, cachedLines: document.logicalLines)
        : moveRight(root, current, stops: stops);
    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 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 pref = _adjustPreferredXForBlockImage(document, current, cursor.preferredX);
      if (key == LogicalKeyboardKey.arrowUp) {
        result = moveUp(root, current, pref,
            document.resolveCaretX, document.resolveCaretY,
            stops: candidateStops, allStops: stops);
      } else {
        result = moveDown(root, current, pref,
            document.resolveCaretX, document.resolveCaretY,
            stops: candidateStops, allStops: stops);
      }
      isVertical = true;
    }
  } else {
    return false;
  }

  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
  _syncSelectionManager(document);

  // Arrow navigation never mutates content: use the cursor-only notification
  // so the cached caret-stop rail and node index survive across key presses.
  document.cursorOnlyUpdate();

  return true;
}