apply<T extends DataGridRow> method
Apply this event's transformation to the state. Returns the new state, or null if no state change should occur. Can return a Future for async operations.
Implementation
@override
DataGridState<T>? apply<T extends DataGridRow>(EventContext<T> context) {
final state = context.state;
final activeCellId = state.selection.activeCellId;
// ── No active cell: fall back to row navigation (up/down only) ──────────
if (activeCellId == null) {
return _rowNavFallback(state);
}
// ── Resolve current position ─────────────────────────────────────────────
final (activeRowId, activeColId) = parseCellId(activeCellId);
final visibleColumns =
state.effectiveColumns.where((c) => c.visible).toList();
// For Shift+Arrow we need the anchor's row/col indices as well as the
// active cell's. Parse the anchor up front so both can be located in a
// single scan of displayOrder below — avoiding a second pass or a HashMap.
final existingPath = state.selection.focusedCells;
final anchorCellId = extend && existingPath.isNotEmpty
? existingPath.first
: null;
final anchorParsed =
anchorCellId != null ? parseCellId(anchorCellId) : null;
final anchorRowId = anchorParsed?.$1;
final anchorColId = anchorParsed?.$2;
// ── Row index resolution ─────────────────────────────────────────────────
// displayOrder can contain hundreds of thousands of entries.
// List.indexOf is O(n) and building a Map<double,int> allocates on the
// heap. Instead we do one forward scan that locates both the active row
// and (when extending) the anchor row simultaneously, breaking as soon
// as both are found. In the common case — both cells visible on screen —
// this exits within the first few dozen iterations.
int rowIndex = -1;
int anchorRowIdx = -1;
for (int i = 0; i < state.displayOrder.length; i++) {
final id = state.displayOrder[i];
if (rowIndex == -1 && id == activeRowId) rowIndex = i;
if (anchorRowId != null && anchorRowIdx == -1 && id == anchorRowId) {
anchorRowIdx = i;
}
if (rowIndex != -1 && (anchorRowId == null || anchorRowIdx != -1)) break;
}
// Column count is always small (tens), so a straightforward scan is fine.
// The same two-target pattern is kept for consistency.
int colIndex = -1;
int anchorColIdx = -1;
for (int i = 0; i < visibleColumns.length; i++) {
final id = visibleColumns[i].id;
if (colIndex == -1 && id == activeColId) colIndex = i;
if (anchorColId != null && anchorColIdx == -1 && id == anchorColId) {
anchorColIdx = i;
}
if (colIndex != -1 && (anchorColId == null || anchorColIdx != -1)) break;
}
if (rowIndex == -1 || colIndex == -1) return null;
// ── Compute target position ──────────────────────────────────────────────
int newRowIndex = rowIndex;
int newColIndex = colIndex;
switch (direction) {
case CellNavDirection.up:
newRowIndex = rowIndex - 1;
case CellNavDirection.down:
newRowIndex = rowIndex + 1;
case CellNavDirection.left:
newColIndex = colIndex - 1;
case CellNavDirection.right:
newColIndex = colIndex + 1;
}
// Clamp to bounds
if (newRowIndex < 0 ||
newRowIndex >= state.displayOrder.length ||
newColIndex < 0 ||
newColIndex >= visibleColumns.length) {
return null;
}
final newCellId =
'${state.displayOrder[newRowIndex]}_${visibleColumns[newColIndex].id}';
// ── Update focused-cells path ────────────────────────────────────────────
if (!extend) {
return state.copyWith(
selection: state.selection.copyWith(focusedCells: [newCellId]),
);
}
// Shift+Arrow: anchor stays fixed, cursor moves to newCell.
// Rebuild rectangle so selection can shrink/grow in any direction.
if (anchorRowIdx == -1 || anchorColIdx == -1) {
return state.copyWith(
selection: state.selection.copyWith(focusedCells: [newCellId]),
);
}
// Traverse anchor→cursor so focusedCells.first == anchor, .last == cursor.
final rStep = newRowIndex >= anchorRowIdx ? 1 : -1;
final cStep = newColIndex >= anchorColIdx ? 1 : -1;
final cells = <String>[];
for (int r = anchorRowIdx; r != newRowIndex + rStep; r += rStep) {
for (int c = anchorColIdx; c != newColIndex + cStep; c += cStep) {
cells.add('${state.displayOrder[r]}_${visibleColumns[c].id}');
}
}
return state.copyWith(
selection: state.selection.copyWith(focusedCells: cells),
);
}