Skip to content

[two_dimensional_scrollables] Exclude trailing pinned spans from the non-pinned range - #12666

Draft
m1roxx wants to merge 1 commit into
flutter:mainfrom
m1roxx:tableview-trailing-pinned-double-layout
Draft

[two_dimensional_scrollables] Exclude trailing pinned spans from the non-pinned range#12666
m1roxx wants to merge 1 commit into
flutter:mainfrom
m1roxx:tableview-trailing-pinned-double-layout

Conversation

@m1roxx

@m1roxx m1roxx commented Aug 28, 2026

Copy link
Copy Markdown

RenderTableViewport lays out and paints the table as nine regions: leading pinned, regular and
trailing pinned rows, each crossed with the same three column categories.
_updateFirstAndLastVisibleCell binary searches for the last regular row and column of the visible
range, and when no regular span reaches the trailing edge of the layout target it fell back to the
last index in the metrics map:

_lastNonPinnedColumn ??= _columnMetrics.length - 1;

Whenever trailingPinnedRowCount or trailingPinnedColumnCount is greater than zero, that last
index is a trailing pinned span. The regular range then overlaps the trailing pinned range, and
the same TableVicinity is visited by two regions in a single layout and paint pass.

_updateColumnMetrics and _updateRowMetrics already cap the same value correctly, with the rule
that _lastRegularColumnIndex and _lastRegularRowIndex express, so the two code paths disagreed.
This PR makes _updateFirstAndLastVisibleCell use those getters. The old fallback is kept for the
case where they are null — infinite spans with no null terminator — where trailing pinned spans
cannot exist, since _firstTrailingPinnedColumn and _firstTrailingPinnedRow both require a
non-null span count.

Fixes flutter/flutter#185842

Beyond the duplicated work

The overlap is not only wasted layout. Scrolling a table to the end with a trailing pinned span
throws, because two regions claim the same cell:

Expected to re-use an element at (row: 0, column: 19), but none was found.
  package:flutter/src/widgets/two_dimensional_viewport.dart:377  'elementToReuse != null'

and, once the spans carry a decoration, the non-pinned region reaches a cell whose paintOffset was
never set for it:

Null check operator used on a null value
  RenderTableViewport._paintCells.getColumnRect  (table.dart:1888)

Dragging through a 60x60 table with pinned and trailing pinned rows and columns throws
Expected to re-use an element at ... and TableViewCell for (row: 1, column: 58) could not be found before this change, and nothing after it. That is the same subsystem and trigger as
flutter/flutter#190800, but the specific assertion reported there did not
reproduce in my runs, so I am not claiming this fixes it.

Tests

Two regression tests are added to table_test.dart, one per axis. Each scrolls a table with a
trailing pinned span to the end, asserts that no exception is thrown, and asserts that the trailing
pinned span's decoration is painted exactly once — by the trailing pinned region only. Both fail
before this change.

Counting cellBuilder calls does not work as a test here: buildOrObtainChildFor caches children
for the frame and RenderObject.layout early-returns on unchanged constraints, so the duplicated
visit is invisible from the delegate. The new CountingSpanDecoration helper observes it through
the public TableSpanDecoration.paint API instead.

Pre-Review Checklist

Footnotes

  1. Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling. 2

…non-pinned range

RenderTableViewport lays out and paints the table as nine regions: leading
pinned, regular and trailing pinned rows crossed with the same three column
categories. _updateFirstAndLastVisibleCell binary searches for the last
regular row and column of the visible range, and when no regular span
reaches the trailing edge of the layout target it fell back to the last
index in the metrics map, which is a trailing pinned span whenever
trailingPinnedRowCount or trailingPinnedColumnCount is greater than zero.

The regular range then overlapped the trailing pinned range, so the same
vicinity was visited by two regions in a single layout and paint pass. That
wastes work, and it also throws: "Expected to re-use an element at ...",
"TableViewCell for ... could not be found", and a null paintOffset while
computing span decoration bounds.

_updateColumnMetrics and _updateRowMetrics already cap the range with the
same rule that _lastRegularColumnIndex and _lastRegularRowIndex express, so
this reuses those getters and keeps the old fallback for the infinite case
where they are null and trailing pinned spans cannot exist.

Fixes flutter/flutter#185842
@github-actions github-actions Bot added p: two_dimensional_scrollables Issues pertaining to the two_dimensional_scrollables package triage-framework Should be looked at in framework triage labels Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

p: two_dimensional_scrollables Issues pertaining to the two_dimensional_scrollables package triage-framework Should be looked at in framework triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[two_dimensional_scrollables] TableView trailing pinned spans are laid out twice

1 participant