refactor: a field round reads the page grid once
Both indexes and the cross-references each read it for themselves, twice per round in the index's case: 59 readings of it over a load where 37 do. The round hands one to every reader instead, so they all resolve their pages against the same grid, and the lazy holders they kept go with it. No measurable time — the style pass a reading forces is owed to the geometry read that follows it either way.
This commit is contained in:
1 parent
fed2ccedad
commit
2f51292587
4 files changed
+17
-19
No files matched your search
@@ -149,7 +149,8 @@ Opening a 460-page document ran five rounds of that where four answer everything
|
||||
**The fields of one answer read together.** Every field that resolves against the settled
|
||||
layout — each index's page numbers, every cross-reference — registers with
|
||||
`scheduleFieldRound(view, owner, read)` (`pageBreaks.ts`) instead of scheduling a frame of
|
||||
its own. The round runs every reader's measurement first and only then their writes, since
|
||||
its own. The round reads the page grid once and hands it to every reader — they all resolve
|
||||
against the same one — and runs every reader's measurement first and only then their writes, since
|
||||
a write between two measurements makes the browser lay the whole document out again; a
|
||||
write may return one more phase (the index reads back where its rows now fall), run after
|
||||
every other write. Their cached results — the index `entries`, a reference's `text` — ride
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { Editor } from '@tiptap/core';
|
||||
import type { Node as PMNode } from '@tiptap/pm/model';
|
||||
import type { EditorView } from '@tiptap/pm/view';
|
||||
import { bookmarks, findBookmark, type BookmarkRef } from './bookmark';
|
||||
import { readVerticalMargins, pageOfElement, scheduleFieldRound, type FieldWrite, type PageGrid } from './pageBreaks';
|
||||
import { pageOfElement, scheduleFieldRound, type FieldWrite, type PageGrid, type VMargins } from './pageBreaks';
|
||||
|
||||
// A cross-reference: an inline atom showing either the text of a bookmark or the page it
|
||||
// sits on, kept live by the node view the way the TOC keeps its page numbers. Round-trips
|
||||
@@ -119,10 +119,10 @@ class CrossRefBatch {
|
||||
|
||||
schedule(): void {
|
||||
if (this.editor.isDestroyed) return;
|
||||
scheduleFieldRound(this.editor.view, this, () => this.measure());
|
||||
scheduleFieldRound(this.editor.view, this, (vm) => this.measure(vm));
|
||||
}
|
||||
|
||||
private measure(): FieldWrite | void {
|
||||
private measure(vm: VMargins): FieldWrite | void {
|
||||
const { editor } = this;
|
||||
if (editor.isDestroyed) return;
|
||||
const view = editor.view;
|
||||
@@ -132,8 +132,7 @@ class CrossRefBatch {
|
||||
}
|
||||
const targets = new Map<string, BookmarkRef>();
|
||||
for (const b of bookmarks(editor.state.doc)) if (!targets.has(b.name)) targets.set(b.name, b);
|
||||
let grid: PageGrid | null = null;
|
||||
const gridOf = () => (grid ??= readVerticalMargins(view.dom as HTMLElement).grid);
|
||||
const gridOf = () => vm.grid;
|
||||
const jobs: { ref: CrossRefView; node: PMNode; pos: number; text: string }[] = [];
|
||||
for (const ref of this.views) {
|
||||
const pos = ref.pos();
|
||||
|
||||
@@ -333,7 +333,7 @@ export const FORCE_PAGE_RECALC = 'forcePageBreakRecalc';
|
||||
// It measures, and what it returns writes: its own DOM, and its cached result on the
|
||||
// round's transaction. A write may return one more phase, run after every other write.
|
||||
export type FieldWrite = (tr: Transaction) => FieldWrite | void;
|
||||
export type FieldRead = () => FieldWrite | void;
|
||||
export type FieldRead = (vm: VMargins) => FieldWrite | void;
|
||||
|
||||
type FieldRound = { jobs: Map<object, FieldRead>; raf: number | null };
|
||||
const fieldRounds = new WeakMap<EditorView, FieldRound>();
|
||||
@@ -352,7 +352,10 @@ function runFieldRound(view: EditorView): void {
|
||||
const round = fieldRounds.get(view);
|
||||
if (!round) return;
|
||||
round.raf = null;
|
||||
let phase = Array.from(round.jobs.values(), (read) => read());
|
||||
// One reading of the page grid for the round: getComputedStyle is a style pass over
|
||||
// the whole document, and every field resolves its pages against the same grid.
|
||||
const vm = readVerticalMargins(view.dom as HTMLElement);
|
||||
let phase = Array.from(round.jobs.values(), (read) => read(vm));
|
||||
round.jobs.clear();
|
||||
const tr = view.state.tr.setMeta('addToHistory', false);
|
||||
// Bounded: measure, write, and the one read-back a write can ask for.
|
||||
|
||||
@@ -10,7 +10,7 @@ import { indexEntries, indexRows } from './indexEntry';
|
||||
import { bibliographyEntries, bibliographyRows } from './bibliographyEntry';
|
||||
import { isCitationStyle, type CitationStyle } from '../../utils/citationStyle';
|
||||
import type { Transaction } from '@tiptap/pm/state';
|
||||
import { readVerticalMargins, pageOfElement, topInEditor, scheduleFieldRound, FORCE_PAGE_RECALC, type FieldWrite, type PageGrid } from './pageBreaks';
|
||||
import { pageOfElement, topInEditor, scheduleFieldRound, FORCE_PAGE_RECALC, type FieldWrite, type PageGrid, type VMargins } from './pageBreaks';
|
||||
|
||||
// A generated index: a block atom listing every source with its live page number — the
|
||||
// headings for a table of contents, the captions of one category for a list of figures
|
||||
@@ -232,7 +232,7 @@ class TocView {
|
||||
|
||||
private schedule(): void {
|
||||
if (this.editor.isDestroyed) return;
|
||||
scheduleFieldRound(this.editor.view, this, () => this.measure());
|
||||
scheduleFieldRound(this.editor.view, this, (vm) => this.measure(vm));
|
||||
}
|
||||
|
||||
// What the index lists, in document order: the headings down to its own level, or —
|
||||
@@ -295,19 +295,15 @@ class TocView {
|
||||
return out;
|
||||
}
|
||||
|
||||
private grid(): PageGrid {
|
||||
return readVerticalMargins(this.editor.view.dom as HTMLElement).grid;
|
||||
}
|
||||
|
||||
private pageOf(pos: number, grid: PageGrid): number {
|
||||
const el = this.editor.view.nodeDOM(pos) as HTMLElement | null;
|
||||
if (!el || el.nodeType !== 1) return 1;
|
||||
return pageOfElement(this.editor.view, el, grid);
|
||||
}
|
||||
|
||||
private measure(): FieldWrite | void {
|
||||
private measure(vm: VMargins): FieldWrite | void {
|
||||
if (this.editor.isDestroyed || !this.dom.isConnected) return;
|
||||
const grid = this.grid();
|
||||
const grid = vm.grid;
|
||||
let heads = this.sources();
|
||||
let entries: TocEntry[];
|
||||
if (indexKindOf(this.node()?.attrs?.index) === 'alphabetical') {
|
||||
@@ -335,18 +331,17 @@ class TocView {
|
||||
}
|
||||
// The rows carry their page numbers now, so where they fall is read once every
|
||||
// field of the round has written.
|
||||
return (last) => this.paginate(last);
|
||||
return (last) => this.paginate(last, vm);
|
||||
};
|
||||
}
|
||||
|
||||
// The index is a block atom: it has no inner document positions for pagination to put
|
||||
// a spacer at, so one longer than a page breaks itself — the row that would cross the
|
||||
// boundary takes the gap to the next page's content top as its margin.
|
||||
private paginate(tr: Transaction): void {
|
||||
private paginate(tr: Transaction, vm: VMargins): void {
|
||||
const rows = Array.from(this.dom.querySelectorAll<HTMLElement>('.toc-entry'));
|
||||
if (!rows.length) return;
|
||||
const view = this.editor.view;
|
||||
const vm = readVerticalMargins(view.dom as HTMLElement);
|
||||
for (const row of rows) row.style.marginTop = '';
|
||||
// Read every natural top and height first: applying a gap moves each row below it,
|
||||
// and one reflow for the whole index beats one per row.
|
||||
|
||||
Reference in new issue
Block a user