fix(library): the wall counts its columns off the layout, not off the run it drew

A column count read back off the cards the window drew is an input the window
feeds itself with: the drawn run is a whole number of rows, and those rows were
read back into the count. A wall hidden behind the studio read nothing but zeroes
off its own cards and reset itself to the top on the way out. Either way a window
was written from a window, pass after pass, until React gave up with #185 and the
whole screen went white — there is nothing between that throw and the floor.

The count now comes off the grid, which is the same read the settle effect above
already scrolls the wall by; a wall with no box is not measured at all, and is
measured on the frame it comes back up; the one reset is written off the shelf's
own length rather than off the last window; the scroller is not a place the
browser may anchor; and a chain of writes past four is cut until something
outside moves — a scroll, a resize, or another shelf.
This commit is contained in:
locpham
2026-10-10 07:43:55 +07:00
parent c314d4608f
commit 6fdd0f3240
2 changed files with 53 additions and 16 deletions
+52 -15
View File
@@ -197,6 +197,12 @@ const WALL_LANES = 2;
// What `.adm-grid` leaves between its rows. A spacer has to stand the same
// distance past the last row it replaces as the row itself would have.
const WALL_GAP = 12;
// How many windows the wall may write in a row before it is talking to itself.
// A window read off the window before it is a loop, and React answers the fifty
// turns of one with #185 and a blank screen; four is more than an honest change
// needs — a re-column takes two passes — and the count is handed back by the
// first scroll, resize or change of shelf the reader makes.
const WALL_CARRIES = 4;
// The hours a frame's own clock can read, and what the time filter offers: the
// whole day in the 24-hour form a camera writes into EXIF.
const HOURS = Array.from({ length: 24 }, (_, i) => i);
@@ -3088,32 +3094,60 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject<HTMLDivElem
// The window is only ever set through here: a `setWin` the wall already
// stands at is a render bought for nothing, and this is measured after every
// render — that is the loop (React #185).
// And the second guard, for a reader the first one cannot see: a window
// written off the window before it is a window that can write itself, and
// whatever the shelf does between the two there are only so many turns of
// that before React gives up with #185 — a blank screen, this app having no
// error boundary between it and the floor. `ponytail:` past the count the
// wall stands still, one window behind, until something outside moves — the
// next scroll, resize, or count hands the pen straight back.
let carries = 0;
const show = (next: { from: number; to: number; above: number; below: number }) => {
const prev = winRef.current;
if (prev.from === next.from && prev.to === next.to && prev.above === next.above && prev.below === next.below) return;
if (carries >= WALL_CARRIES) return;
carries++;
winRef.current = next;
setWin(next);
};
const measure = () => {
const cards = el.querySelectorAll<HTMLElement>(':scope > .adm-card');
const first = cards[0];
let cols = 0;
if (first) while (cols < cards.length && cards[cols].offsetTop === first.offsetTop) cols++;
// One row, or a wall not yet drawn: open on the first screenful and let the
// next pass read the step off what it drew.
if (cards.length <= cols) {
const to = Math.min(count, 24);
show({ from: 0, to, above: 0, below: 0 });
// A wall that is not standing has no box to measure: the library is
// `display:none` while the studio is up, and a window read off it would be
// a window read off nothing — every card reports `offsetTop` 0 and every
// scroll 0 — which threw the shelf back to its top every time the studio
// handed back to it. It is read on the frame it comes back up, which is a
// box change.
if (!el.clientHeight) return;
// And a window that is nowhere on the shelf stands nowhere: outside a shelf
// that shrank under it — another folder, a filter, a shorter roll — or empty
// while the shelf is not, which is the first paint, before the catalogue has
// landed. This is the one reset, and it is written off the shelf's own
// length rather than off the cards the last window drew, which is what made
// the reset a window the window fed.
const held = winRef.current.to - winRef.current.from;
if (held <= 0 || winRef.current.from >= count || winRef.current.to > count) {
show({ from: 0, to: Math.min(count, 24), above: 0, below: 0 });
return;
}
// How many columns is the grid's own business, read off its layout and not
// counted off the cards the window drew: the drawn run is a whole number of
// rows, those rows were read back into the count, and a count read off the
// run is a count the window feeds itself with. This is the same read the
// settle effect above scrolls the wall by.
const cols = Math.max(1, getComputedStyle(el).gridTemplateColumns.split(' ').length);
// The run the step below is read off. A run of a single row is no run at
// all — that is the last row of a shelf scrolled to its end — and a wall
// that has drawn nothing is nothing to read: the state's own first
// screenful stands until there is one.
const cards = el.querySelectorAll<HTMLElement>(':scope > .adm-card');
const rowsDrawn = Math.floor(cards.length / cols);
if (rowsDrawn < 2) return;
// The step from one row to the next, taken as the drawn run's own average.
// The cards do NOT all stand the same height — a caption that wraps makes
// its row taller — and `offsetTop` is rounded to whole pixels, so the
// smallest step would be a pixel short on every one of thousands of rows.
// The average of the rows in hand is the honest guess at the ones that are
// not in hand.
const rowsDrawn = Math.floor(cards.length / cols);
if (rowsDrawn < 2) return;
// The step is read off the drawn rows the first time the wall stands at this
// many columns, and kept until it stands at another: the average of a run of
// rows moves as the run grows, so a step re-read on every pass made the window
@@ -3144,13 +3178,16 @@ function useWallWindow(count: number, on: boolean): { ref: RefObject<HTMLDivElem
show({ from, to, above, below });
};
measureRef.current = measure;
const onScroll = () => measure();
el.addEventListener('scroll', onScroll, { passive: true });
const ro = new ResizeObserver(onScroll);
const onOutside = () => {
carries = 0;
measure();
};
el.addEventListener('scroll', onOutside, { passive: true });
const ro = new ResizeObserver(onOutside);
ro.observe(el);
return () => {
measureRef.current = () => {};
el.removeEventListener('scroll', onScroll);
el.removeEventListener('scroll', onOutside);
ro.disconnect();
};
}, [count, on]);
+1 -1
View File
@@ -1342,7 +1342,7 @@ button.hint:disabled { cursor: default; }
}
/* All thumbs: the wall scrolls down inside the stage, and its own scrollbar on
the right edge is what reaches the frames below. */
.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; }
.lib-root .adm-grid { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding-right: 6px; overflow-anchor: none; }
/* What the thumbnail wall is read through: the four selects and the count of what
is left. One row above the wall, and it does not scroll with it. */
.lib-root .adm-stage > .lib-filters,