Skip to content

Commit cbee22b

Browse files
committed
the big flat tree refactor
The motivation is to have one array represent the entire layout tree rather than parent boxes storing children arrays. I started down this rabbit hole in December after I realized text-align: justify needs to bsearch inline content to reapply spacing after wrapping and reshaping. It is a much more memory-efficient layout: up to 3% less memory is used in CellEngine. I learned this technique from Chrome: > The flat list is created for each inline formatting context in the > order of a depth-first search of its inline layout subtree. Each entry > in the list is a tuple of (object, number of descendants) https://developer.chrome.com/docs/chromium/renderingng-data-structures#inline_fragment_items For dropflow, it makes more sense to store the last descendant's index instead of the number of descendants, since we maintain a parent stack during iteration. The index of the box itself doesn't necessarily need to be stored - usually you have the index during iteration - but I found passing it around to be cumbersome, and it makes for a much better .id! This refactor also includes a much more efficient strategy for handling mixed block and inline content, documented in the comment. Generating boxes was deeply affected due to the new way of storing trees. Whitespace collapsing was also deeply affected since it can remove tree nodes. It now happens in the generate phase, which is now called flow.layout instead of flow.generate, and the old flow.layout is now called flow.reflow. I want to use "layout" as a noun from now on, and "reflow" for the verb, which is about what Firefox does. The new Layout class will eventually store flat arrays of items and fragments, not just the tree nodes. Reflow is faster, mainly due to the removal of whitespace collapsing. The new memory layout had slightly disappointing results, although it did lead to fewer CPU cache misses, but the speedup was nothing to remark about, except in perf-2 where it's a solid 2-3% faster. Painting also got faster.
1 parent 3c3d245 commit cbee22b

28 files changed

+1570
-1300
lines changed

README.md

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ const spanStyle = flow.style({
115115
});
116116

117117
// Create a DOM
118-
const rootElement = flow.dom(
118+
const el = flow.dom(
119119
flow.h('div', {style: divStyle}, [
120120
'Hello, ',
121121
flow.h('span', {style: spanStyle}, ['World!'])
@@ -124,7 +124,7 @@ const rootElement = flow.dom(
124124

125125
// Layout and paint into the entire canvas (see also renderToCanvasContext)
126126
const canvas = createCanvas(250, 50);
127-
await flow.renderToCanvas(rootElement, canvas);
127+
await flow.renderToCanvas(el, canvas);
128128

129129
// Save your image
130130
fs.writeFileSync(new URL('file:///hello.png'), canvas.toBuffer());
@@ -152,14 +152,14 @@ const roboto1 = new flow.FontFace('Roboto', new URL('file:///Roboto-Regular.ttf'
152152
const roboto2 = new flow.FontFace('Roboto', new URL('file:///Roboto-Bold.ttf'), {weight: 700});
153153
flow.fonts.add(roboto1).add(roboto2);
154154

155-
const rootElement = parse(`
155+
const el = parse(`
156156
<div style="background-color: #1c0a00; color: #b3c890; text-align: center;">
157157
Hello, <span style="color: #73a9ad; font-weight: bold;">World!</span>
158158
</div>
159159
`);
160160

161161
const canvas = createCanvas(250, 50);
162-
flow.renderToCanvas(rootElement, canvas);
162+
flow.renderToCanvas(el, canvas);
163163

164164
canvas.createPNGStream().pipe(fs.createWriteStream(new URL('hello.png', import.meta.url)));
165165
```
@@ -185,12 +185,12 @@ Then, you can either render the DOM into a canvas using its size as the viewport
185185

186186
1. [Render DOM to canvas](#render-dom-to-canvas)
187187

188-
Or, you can use the lower-level functions to retain the layout, in case you want to re-layout at a different size, choose not to paint (for example if the layout isn't visible) or get intrinsics:
188+
Or, you can use the lower-level functions to retain the layout, in case you want to reflow at a different size, choose not to paint (for example if the layout isn't visible) or get intrinsics:
189189

190190
1. [Load dependent resources](#load)
191-
2. [Generate a tree of layout boxes from the DOM](#generate)
192-
3. [Layout the box tree](#layout)
193-
4. [Paint the box tree to a target like canvas](#paint)
191+
2. [Create a layout for the DOM](#layout)
192+
3. [Reflow the layout](#layout)
193+
4. [Paint the layout to a target like HTML5 canvas](#paint)
194194

195195
## Fonts
196196

@@ -378,7 +378,7 @@ Parses HTML. If you don't specify a root `<html>` element, content will be wrapp
378378
This is only for simple use cases. For more advanced usage continue on to the next section.
379379

380380
```ts
381-
function renderToCanvas(rootElement: HTMLElement, canvas: Canvas): Promise<void>;
381+
function renderToCanvas(el: HTMLElement, canvas: Canvas): Promise<void>;
382382
```
383383

384384
Renders the whole layout to the canvas, using its width and height as the viewport size.
@@ -393,7 +393,7 @@ class Image {
393393
reason: unknown;
394394
}
395395
396-
function load(rootElement: HTMLElement): Promise<LoadableResource[]>;
396+
function load(el: HTMLElement): Promise<LoadableResource[]>;
397397
```
398398

399399
Ensures that all of the fonts and images required by the document are loaded.
@@ -405,7 +405,7 @@ For images, it means fetching the image data so it's ready for layout. In additi
405405
Because the whole fallback list for every unique set of font properties is appended to the returned loaded list, it may contain duplicate fonts. And since there is only one `Image` instance per unique URL, there may be duplicate images. Deduplication is left up to the caller since it impacts performance. The list is in document order.
406406

407407
```ts
408-
function loadSync(rootElement: HTMLElement): LoadableResource[];
408+
function loadSync(el: HTMLElement): LoadableResource[];
409409
```
410410

411411
If your URLs are all file:/// URLs in Node/Bun, `loadSync` can be used to load dependencies.
@@ -421,38 +421,42 @@ function revokeObjectURL(url: string): void;
421421

422422
These functions can be used to send image buffers to `<img src>`. Since there is no associated document (unlike the browser), memory is retained between `createObjectURL` and `revokeObjectURL`.
423423

424-
## Generate
424+
## Create a layout
425425

426-
### `generate`
426+
### `layout`
427427

428428
```ts
429-
function generate(rootElement: HTMLElement): BlockContainer
429+
class Layout {
430+
root(): BlockContainer;
431+
}
432+
433+
function layout(el: HTMLElement): Layout;
430434
```
431435

432-
Generates a box tree for the element tree. Box trees roughly correspond to DOM trees, but usually have more boxes (like for anonymous text content between block-level elements (`div`s)) and sometimes fewer (like for `display: none`).
436+
Creates a layout, which consists of a box tree (the input to layout) as well as the fragmentation tree and glyphs (the output of layout). You need to call [`flow.reflow`](#reflow) at least once before painting or getting intrinsics.
433437

434-
`BlockContainer` has a `repr()` method for logging the tree.
438+
Box trees roughly correspond to DOM trees, but usually have more boxes (like for anonymous text content between block-level elements (`div`s)) and sometimes fewer (like for `display: none`).
435439

436440
Hold on to the return value so you can lay it out many times in different sizes, paint it or don't paint it if it's off-screen, or get intrinsics to build a higher-level logical layout (for example, spreadsheet column or row size even if the content is off screen).
437441

438-
## Layout
442+
## Reflowing a layout
439443

440-
### `layout`
444+
### `reflow`
441445

442446
```ts
443-
function layout(root: BlockContainer, width = 640, height = 480);
447+
function reflow(layout: Layout, width = 640, height = 480);
444448
```
445449

446450
Position boxes and split text into lines so the layout tree is ready to paint. Can be called over and over with a different viewport size.
447451

448-
In more detail, layout involves:
452+
In more detail, reflowing involves:
449453

450454
* Margin collapsing for block boxes
451455
* Passing text to HarfBuzz, iterating font fallbacks, wrapping, reshaping depending on break points
452456
* Float placement and `clear`ing
453457
* Positioning shaped text spans and backgrounds according to `direction` and text direction
454-
* Second and third pass layouts for intrinsics of `float`, `inline-block`, and `absolute`s
455-
* Post-layout positioning (`position`)
458+
* Calculating intrinsics for the content of `float`s, `inline-block`s, and `absolute`s
459+
* Post normal flow positioning (`position`)
456460

457461
## Paint
458462

@@ -465,31 +469,31 @@ There is also a toy HTML target that was used early on in development, and kept
465469
### `paintToCanvas`
466470

467471
```ts
468-
function paintToCanvas(root: BlockContainer, ctx: CanvasRenderingContext2D): void;
472+
function paintToCanvas(root: Layout, ctx: CanvasRenderingContext2D): void;
469473
```
470474

471475
Paints the layout to a browser canvas, node-canvas, or similar standards-compliant context.
472476

473477
### `paintToSvg`
474478

475479
```ts
476-
function paintToSvg(root: BlockContainer): string;
480+
function paintToSvg(root: Layout): string;
477481
```
478482

479483
Paints the layout to an SVG string, with `@font-face` rules referencing the URL you passed to `flow.FontFace`.
480484

481485
### `paintToSvgElements`
482486

483487
```ts
484-
function paintToSvgElements(root: BlockContainer): string;
488+
function paintToSvgElements(root: Layout): string;
485489
```
486490

487491
Similar to `paintToSvg`, but doesn't add `<svg>` or `@font-face` rules. Useful if you're painting inside of an already-existing SVG element.
488492

489493
### `paintToHtml`
490494

491495
```ts
492-
function paintToHtml(root: BlockContainer): string;
496+
function paintToHtml(root: Layout): string;
493497
```
494498

495499
Paint to HTML! Yes, this API can actually be used to go from HTML to HTML. It generates a flat list of a bunch of absolutely positioned elements. Probably don't use this, but it can be useful in development and is amusing.
@@ -529,8 +533,8 @@ Typically when you use `query`, you'll be getting a `BlockContainer`:
529533

530534
```ts
531535
const dom = parse('<div id="d" style="width: 100px; height: 100px;"></div>');
532-
const root = flow.generate(dom);
533-
flow.layout(root, 200, 200);
536+
const layout = flow.layout(dom);
537+
flow.reflow(layout, 200, 200);
534538
const [box] = dom.query('#d')!.boxes as flow.BlockContainer[];
535539
box.getContentArea().width; // 100
536540
box.getContentArea().height; // 100

examples/bidi-1.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ const rootElement = parse(`
1919
</html>
2020
`);
2121

22-
const blockContainer = flow.generate(rootElement);
22+
const layout = flow.layout(rootElement);
2323

24-
blockContainer.log();
24+
flow.log(layout);
2525
flow.loadSync(rootElement);
2626

2727
const canvas = createCanvas(200, 250);
28-
flow.layout(blockContainer, canvas.width, canvas.height);
28+
flow.reflow(layout, canvas.width, canvas.height);
2929
const ctx = canvas.getContext('2d');
30-
flow.paintToCanvas(blockContainer, ctx);
30+
flow.paintToCanvas(layout, ctx);
3131
fs.writeFileSync(new URL('bidi-1.png', import.meta.url), canvas.toBuffer());

examples/fallbacks-1.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ const rootElement = parse(`
1717
</div>
1818
`);
1919

20-
const blockContainer = flow.generate(rootElement);
20+
const layout = flow.layout(rootElement);
2121

2222
flow.loadSync(rootElement);
23-
blockContainer.log();
23+
flow.log(layout);
2424

2525
const canvas = createCanvas(400, 150);
26-
flow.layout(blockContainer, canvas.width, canvas.height);
26+
flow.reflow(layout, canvas.width, canvas.height);
2727

2828
const ctx = canvas.getContext('2d');
29-
flow.paintToCanvas(blockContainer, ctx);
29+
flow.paintToCanvas(layout, ctx);
3030
fs.writeFileSync(new URL('fallbacks-1.png', import.meta.url), canvas.toBuffer());

examples/images-1.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,11 @@ const rootElement = flow.dom(
7474

7575
// Normal layout, logging
7676
await flow.load(rootElement);
77-
const blockContainer = flow.generate(rootElement);
78-
flow.layout(blockContainer, 1200, 1800);
79-
blockContainer.log({containingBlocks: true});
77+
const layout = flow.layout(rootElement);
78+
flow.reflow(layout, 1200, 1800);
79+
flow.log(layout, undefined, {containingBlocks: true});
8080
const canvas = createCanvas(1200, 1800);
8181
const ctx = canvas.getContext('2d');
82-
flow.paintToCanvas(blockContainer, ctx);
82+
flow.paintToCanvas(layout, ctx);
8383

8484
fs.writeFileSync(new URL('images-1.png', import.meta.url), canvas.toBuffer());

examples/inlines-1.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ const rootElement = parse(`
2727

2828
flow.loadSync(rootElement);
2929

30-
const blockContainer = flow.generate(rootElement);
31-
blockContainer.log({css: 'zoom'});
30+
const layout = flow.layout(rootElement);
31+
flow.log(layout, undefined, {css: 'zoom'});
3232

3333
const canvas = createCanvas(600, 400);
34-
flow.layout(blockContainer, canvas.width, canvas.height);
34+
flow.reflow(layout, canvas.width, canvas.height);
3535

3636
const ctx = canvas.getContext('2d');
37-
flow.paintToCanvas(blockContainer, ctx);
37+
flow.paintToCanvas(layout, ctx);
3838
fs.writeFileSync(new URL('inlines-1.png', import.meta.url), canvas.toBuffer());

examples/perf-1.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,32 +48,32 @@ flow.loadSync(rootElement);
4848
const canvas = createCanvas(1600, 1600);
4949
const ctx = canvas.getContext('2d');
5050

51-
const blockContainer = flow.generate(rootElement);
52-
flow.layout(blockContainer, canvas.width, canvas.height);
51+
const layout = flow.layout(rootElement);
52+
flow.reflow(layout, canvas.width, canvas.height);
5353
ctx.clearRect(0, 0, canvas.width, canvas.height);
54-
flow.paintToCanvas(blockContainer, ctx);
54+
flow.paintToCanvas(layout, ctx);
5555
fs.writeFileSync(new URL('perf-1.png', import.meta.url), canvas.toBuffer());
5656

5757
bench('altogether', () => {
58-
const blockContainer = flow.generate(rootElement);
58+
const layout = flow.layout(rootElement);
5959
flow.clearWordCache();
60-
flow.layout(blockContainer, canvas.width, canvas.height);
60+
flow.reflow(layout, canvas.width, canvas.height);
6161
ctx.clearRect(0, 0, canvas.width, canvas.height);
62-
flow.paintToCanvas(blockContainer, ctx);
62+
flow.paintToCanvas(layout, ctx);
6363
}).gc('inner');
6464

65-
bench('generate', () => {
66-
do_not_optimize(flow.generate(rootElement));
65+
bench('flow.layout', () => {
66+
do_not_optimize(flow.layout(rootElement));
6767
}).gc('inner');
6868

69-
bench('layout', () => {
69+
bench('flow.reflow', () => {
7070
flow.clearWordCache();
71-
flow.layout(blockContainer, canvas.width, canvas.height);
71+
flow.reflow(layout, canvas.width, canvas.height);
7272
}).gc('inner');
7373

74-
bench('paint', () => {
74+
bench('flow.paint', () => {
7575
ctx.clearRect(0, 0, canvas.width, canvas.height);
76-
flow.paintToCanvas(blockContainer, ctx);
76+
flow.paintToCanvas(layout, ctx);
7777
}).gc('inner');
7878

7979
await run();

examples/perf-2.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1910,33 +1910,33 @@ flow.loadSync(rootElement);
19101910
const canvas = createCanvas(800 , 28696 );
19111911
const ctx = canvas.getContext('2d');
19121912

1913-
const blockContainer = flow.generate(rootElement);
1914-
flow.layout(blockContainer, 800, 28696);
1913+
const layout = flow.layout(rootElement);
1914+
flow.reflow(layout, 800, 28696);
19151915
ctx.clearRect(0, 0, 800 , 28696 );
1916-
flow.paintToCanvas(blockContainer, ctx);
1916+
flow.paintToCanvas(layout, ctx);
19171917

19181918
fs.writeFileSync(new URL('perf-2.png', import.meta.url), canvas.toBuffer());
19191919

19201920
bench('altogether', () => {
1921-
const blockContainer = flow.generate(rootElement);
1921+
const layout = flow.layout(rootElement);
19221922
flow.clearWordCache();
1923-
flow.layout(blockContainer, 800, 28696);
1923+
flow.reflow(layout, 800, 28696);
19241924
ctx.clearRect(0, 0, 800 , 28696 );
1925-
flow.paintToCanvas(blockContainer, ctx);
1925+
flow.paintToCanvas(layout, ctx);
19261926
}).gc('inner');
19271927

1928-
bench('generate', () => {
1929-
do_not_optimize(flow.generate(rootElement));
1928+
bench('flow.layout', () => {
1929+
do_not_optimize(flow.layout(rootElement));
19301930
}).gc('inner');
19311931

1932-
bench('layout', () => {
1932+
bench('flow.reflow', () => {
19331933
flow.clearWordCache();
1934-
flow.layout(blockContainer, 800, 28696);
1934+
flow.reflow(layout, 800, 28696);
19351935
}).gc('inner');
19361936

1937-
bench('paint', () => {
1937+
bench('flow.paint', () => {
19381938
ctx.clearRect(0, 0, 800 , 28696 );
1939-
flow.paintToCanvas(blockContainer, ctx);
1939+
flow.paintToCanvas(layout, ctx);
19401940
}).gc('inner');
19411941

19421942
await run();

examples/perf-3.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,18 @@ const html = flow.dom(
2626
flow.h('html', {style}, words[Math.floor(Math.random() * words.length)])
2727
);
2828
flow.loadSync(html);
29-
const blockContainer = flow.generate(html);
30-
flow.layout(blockContainer, 100, 20);
31-
flow.paintToCanvas(blockContainer, ctx);
29+
const layout = flow.layout(html);
30+
flow.reflow(layout, 100, 20);
31+
flow.paintToCanvas(layout, ctx);
3232
fs.writeFileSync(new URL('perf-3.png', import.meta.url), canvas.toBuffer());
3333

3434
bench('altogether', () => {
3535
const html = flow.dom(
3636
flow.h('html', {style}, words[Math.floor(Math.random() * words.length)])
3737
);
38-
const blockContainer = flow.generate(html);
38+
const layout = flow.layout(html);
3939
flow.clearWordCache();
40-
flow.layout(blockContainer, 100, 20);
40+
flow.reflow(layout, 100, 20);
4141
}).gc('inner');
4242

4343
bench('dom', () => {
@@ -47,14 +47,14 @@ bench('dom', () => {
4747
do_not_optimize(html);
4848
}).gc('inner');
4949

50-
bench('generate', () => {
51-
const blockContainer = flow.generate(html);
52-
do_not_optimize(blockContainer);
50+
bench('flow.layout', () => {
51+
const layout = flow.layout(html);
52+
do_not_optimize(layout);
5353
}).gc('inner');
5454

55-
bench('layout', () => {
55+
bench('flow.reflow', () => {
5656
flow.clearWordCache();
57-
flow.layout(blockContainer, 100, 20);
57+
flow.reflow(layout, 100, 20);
5858
}).gc('inner');
5959

6060
await run();

0 commit comments

Comments
 (0)