executeHandleArrowKey function
bool
executeHandleArrowKey(
- LogicalKeyboardKey key,
- FluentDocument document, {
- bool ctrl = false,
- 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;
}