fix: reserve the group rows monotonically, and bulk the ancestry compare
Copilot's fifth pass; both correct. **Inline group rows are back in `frameCost`.** Omitting them was the same failure I had just fixed for the header block: they take real table height, so any viewport containing one asked for more data rows than `#grid` could show, and the DOM takes its own scroll position while the client's cursor offsets come from the larger window. Copilot's own suggestion answers the oscillation objection — the reservation is a **high-water mark**, which cannot oscillate because it only rises, and it rises in the safe direction: over-reserving leaves blank space, under-reserving is the bug. It resets on a view change so one dense document does not make every later view pessimistic. **`same_prefix` is replaced by `AxisFrame::shares_ancestry`**, the bulk form, for the same reason `header_labels` and `groups` are bulk: called per (column, level) it scanned the frame twice apiece, quadratic in the columns drawn and growing with the horizontal offset, where the pre-anr9 widget's materialized lookup was O(1). `same_prefix` is **deleted** rather than kept beside it — two answers to one question is the thing this PR exists to remove, and keeping a per-position form that nothing calls would be an invitation. `shared_ancestry_widens_as_ the_level_rises` checks both arms, because a version comparing whole tuples passes the leaf arm and a version comparing only the outermost label passes the outer one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+25
-12
@@ -116,8 +116,9 @@ function setShellStatus(text) {
|
||||
//
|
||||
// It is measured off the painted model rather than predicted, and defaults to
|
||||
// the old one-and-one before the first paint, which is the only moment there
|
||||
// is nothing to measure.
|
||||
let frameCost = { rows: 1, cols: 1 };
|
||||
// is nothing to measure. `groups` is kept as its own high-water mark — see
|
||||
// the tail of `repaint`.
|
||||
let frameCost = { view: null, rows: 1, cols: 1, groups: 0 };
|
||||
|
||||
/** The extent this window can show, in whole DATA cells. */
|
||||
function extent() {
|
||||
@@ -258,18 +259,30 @@ function repaint() {
|
||||
el.categories.replaceChildren(categoryList(model));
|
||||
|
||||
// What the frame costs, so the NEXT viewport announcement is honest: the
|
||||
// header lines, plus a group line if there is one, plus the row header's
|
||||
// width in columns.
|
||||
// header lines, a group line if there is one, the row header's width in
|
||||
// columns, and every inline group row.
|
||||
//
|
||||
// Inline group ROWS are deliberately not counted, even though they take
|
||||
// space. They depend on how many rows the window asked for, so counting
|
||||
// them would make this a feedback loop — a smaller window fetches fewer
|
||||
// rows, which crosses fewer groups, which asks for a larger window. The
|
||||
// three facts below depend only on the axes, so a repaint cannot chase
|
||||
// its own tail. The cost is a little over-fetch, which is the direction
|
||||
// ROW_PX already errs in.
|
||||
// The group rows are the awkward one, because they depend on how many
|
||||
// rows the window asked for: counted directly they are a feedback loop —
|
||||
// a smaller window crosses fewer groups and asks for a larger one, which
|
||||
// crosses more. So the reservation is a HIGH-WATER MARK, which cannot
|
||||
// oscillate because it only ever rises, and it rises in the safe
|
||||
// direction: over-reserving asks for fewer rows than fit and leaves
|
||||
// blank space, where under-reserving asks for more rows than the table
|
||||
// can show and is the off-screen-selection bug itself. It resets when
|
||||
// the view changes, so a dense document does not make every later view
|
||||
// pessimistic.
|
||||
if (model.view_name !== frameCost.view) {
|
||||
frameCost = { view: model.view_name, rows: 1, cols: 1, groups: 0 };
|
||||
}
|
||||
const groups = Math.max(
|
||||
frameCost.groups,
|
||||
model.rows.reduce((n, row) => n + (row.kind === "group" ? 1 : 0), 0),
|
||||
);
|
||||
const next = {
|
||||
rows: model.col_levels.length + (model.col_groups.length > 0 ? 1 : 0),
|
||||
view: model.view_name,
|
||||
groups,
|
||||
rows: model.col_levels.length + (model.col_groups.length > 0 ? 1 : 0) + groups,
|
||||
cols: model.row_levels,
|
||||
};
|
||||
if (next.rows !== frameCost.rows || next.cols !== frameCost.cols) {
|
||||
|
||||
Reference in New Issue
Block a user