Let's build browser engine! in typescript vol4 Layout Tree to Display List
Murahashi [Matt] Kenichi
Posted on April 30, 2019
Layout Tree to Display List.
// node_modules/.bin/ts-node example/layout-tree.ts | display
import * as Jimp from "jimp";
import { BoxType, Dimensions, EdgeSizes, LayoutBox, Rect } from "../src/layout";
import { paint } from "../src/painting";
import { Color, CssValue } from "../src/css";
import { StyledNode } from "../src/style";
import { DomNode } from "../src/dom";
const red = new Color(255, 0, 0, 255);
const green = new Color(0, 255, 0, 255);
const canvas = paint(
new LayoutBox(
new Dimensions(
new Rect(3, 3, 2, 1),
new EdgeSizes(1, 1, 1, 1),
new EdgeSizes(1, 1, 1, 1),
new EdgeSizes(1, 1, 1, 1)
),
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([
["border-color", new CssValue.ColorValue(red)],
["background", new CssValue.ColorValue(green)]
]),
[]
)
),
[]
),
new Rect(0, 0, 8, 7)
);
Jimp.create(canvas.width, canvas.height)
.then((value) => {
let buffer = value.bitmap.data;
for (let i = 0; i < canvas.pixels.length; i++) {
buffer[i * 4] = canvas.pixels[i].r;
buffer[i * 4 + 1] = canvas.pixels[i].g;
buffer[i * 4 + 2] = canvas.pixels[i].b;
buffer[i * 4 + 3] = canvas.pixels[i].a;
}
return value.getBufferAsync(Jimp.MIME_PNG);
})
.then((value) => {
process.stdout.write(value);
})
.catch((error) => {
console.error(error);
});
Let's go!
- StyledNode#value
- getColor
- renderBackground
- renderBorder
- renderLayoutBox
- buildDisplayList
StyledNode#value is necessary for render.
value(name: string): CssValue | null
test("styled node hits", () => {
const expected = new CssValue.Keyword("example");
expect(
new StyledNode(new DomNode(), new Map([["target", expected]]), []).value("target")
).toEqual(expected);
});
test("styled node does not hit", () => {
expect(
new StyledNode(new DomNode(), new Map([["some", new CssValue.Keyword("example")]]), []).value(
"different"
)
).toEqual(null);
});
export class StyledNode {
(snip)
value(name: string): CssValue | null {
return this.specifiedValues.get(name) || null;
}
}
getColor(layoutBox: LayoutBox, name: string): Color | null
is also useful.
test("get color", () => {
const expectedColor = new Color(255, 255, 255, 255);
expect(
getColor(
LayoutBox.Create(
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([["target", new CssValue.ColorValue(expectedColor)]]),
[]
)
)
),
"target"
)
).toEqual(expectedColor);
});
test("get no color", () => {
expect(
getColor(
LayoutBox.Create(new BoxType.BlockNode(new StyledNode(new DomNode(), new Map([]), []))),
"target"
)
).toEqual(null);
});
test("get no color2", () => {
expect(getColor(LayoutBox.Create(new BoxType.AnonymousBlock()), "target")).toEqual(null);
});
test("get no color3", () => {
const notColor = new CssValue.Keyword("example");
expect(
getColor(
LayoutBox.Create(
new BoxType.BlockNode(new StyledNode(new DomNode(), new Map([["target", notColor]]), []))
),
"target"
)
).toEqual(null);
});
export function getColor(layoutBox: LayoutBox, name: string): Color | null {
switch (layoutBox.boxType.format) {
case BoxType.Format.BlockNode:
case BoxType.Format.InlineNode:
const style = layoutBox.boxType.styledNode.value(name);
if (style === null) {
return null;
}
switch (style.format) {
case CssValue.Format.ColorValue:
return style.colorValue;
default:
return null;
}
case BoxType.Format.AnonymousBlock:
return null;
}
}
renderBackground(list: DisplayList, layoutBox: LayoutBox)
is key feature.
test("render background with color", () => {
const displayList: DisplayCommand[] = [];
renderBackground(
displayList,
new LayoutBox(
new Dimensions(
new Rect(20, 30, 5, 5),
new EdgeSizes(2, 3, 4, 5),
new EdgeSizes(2, 3, 4, 5),
new EdgeSizes(2, 3, 4, 5)
),
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([["background", new CssValue.ColorValue(new Color(0, 0, 0, 0))]]),
[]
)
),
[]
)
)
;
expect(displayList[0]).toEqual(
new DisplayCommand.SolidColor(new Color(0, 0, 0, 0), new Rect(16, 22, 15, 23))
;
});
test("render background no color", () => {
const displayList: DisplayCommand[] = [];
renderBackground(
displayList,
LayoutBox.Create(new BoxType.BlockNode(new StyledNode(new DomNode(), new Map(), [])))
);
expect(displayList.length).toEqual(0);
});
export function renderBackground(list: DisplayList, layoutBox: LayoutBox) {
const color = getColor(layoutBox, "background");
if (!color) {
return;
}
list.push(new DisplayCommand.SolidColor(color, layoutBox.dimensions.borderBox()));
}
renderBorders(list: DisplayList, layoutBox: LayoutBox)
is also key feature.
test("render border no border", () => {
const displayList: DisplayCommand[] = [];
renderBorders(
displayList,
LayoutBox.Create(new BoxType.BlockNode(new StyledNode(new DomNode(), new Map(), [])))
);
expect(displayList.length).toEqual(0);
});
test("render border with color", () => {
const displayList: DisplayCommand[] = [];
renderBorders(
displayList,
new LayoutBox(
exampleDimensions,
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([["border-color", new CssValue.ColorValue(black)]]),
[]
)
),
[]
)
);
expect(displayList).toEqual([
// left
new DisplayCommand.SolidColor(black, new Rect(16, 22, 2, 23)),
// right
new DisplayCommand.SolidColor(black, new Rect(28, 22, 3, 23)),
// top
new DisplayCommand.SolidColor(black, new Rect(16, 22, 15, 4)),
// bottom
new DisplayCommand.SolidColor(black, new Rect(16, 40, 15, 5))
]);
});
export function renderBorders(list: DisplayList, layoutBox: LayoutBox) {
const color = getColor(layoutBox, "border-color");
if (!color) {
return;
}
const dimensions = layoutBox.dimensions;
const borderBox = dimensions.borderBox();
// left border
list.push(
new DisplayCommand.SolidColor(
color,
new Rect(borderBox.x, borderBox.y, dimensions.border.left, borderBox.height)
)
);
// right border
list.push(
new DisplayCommand.SolidColor(
color,
new Rect(
borderBox.x + borderBox.width - dimensions.border.right,
borderBox.y,
dimensions.border.right,
borderBox.height
)
)
);
// top border
list.push(
new DisplayCommand.SolidColor(
color,
new Rect(borderBox.x, borderBox.y, borderBox.width, dimensions.border.top)
)
);
// bottom border
list.push(
new DisplayCommand.SolidColor(
color,
new Rect(
borderBox.x,
borderBox.y + borderBox.height - dimensions.border.bottom,
borderBox.width,
dimensions.border.bottom
)
)
);
}
renderLayoutBox(list: DisplayList, layoutBox: LayoutBox)
is recursively applied.
test("render layout box", () => {
const displayList: DisplayCommand[] = [];
renderLayoutBox(
displayList,
new LayoutBox(
exampleDimensions,
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([
["border-color", new CssValue.ColorValue(black)],
["background", new CssValue.ColorValue(blue)]
]),
[]
)
),
[]
)
);
expect(displayList).toEqual([
// background
new DisplayCommand.SolidColor(blue, new Rect(16, 22, 15, 23)),
// left
new DisplayCommand.SolidColor(black, new Rect(16, 22, 2, 23)),
// right
new DisplayCommand.SolidColor(black, new Rect(28, 22, 3, 23)),
// top
new DisplayCommand.SolidColor(black, new Rect(16, 22, 15, 4)),
// bottom
new DisplayCommand.SolidColor(black, new Rect(16, 40, 15, 5))
]);
});
test("render layout box children", () => {
const displayList: DisplayCommand[] = [];
renderLayoutBox(
displayList,
new LayoutBox(
exampleDimensions,
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([
["border-color", new CssValue.ColorValue(black)],
["background", new CssValue.ColorValue(blue)]
]),
[]
)
),
[
new LayoutBox(
new Dimensions(
new Rect(22, 32, 1, 1),
new EdgeSizes(0, 0, 0, 0),
new EdgeSizes(0, 0, 0, 0),
new EdgeSizes(0, 0, 0, 0)
),
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([
["border-color", new CssValue.ColorValue(red)],
["background", new CssValue.ColorValue(green)]
]),
[]
)
),
[]
)
]
)
);
expect(displayList).toEqual([
// background
new DisplayCommand.SolidColor(blue, new Rect(16, 22, 15, 23)),
// left
new DisplayCommand.SolidColor(black, new Rect(16, 22, 2, 23)),
// right
new DisplayCommand.SolidColor(black, new Rect(28, 22, 3, 23)),
// top
new DisplayCommand.SolidColor(black, new Rect(16, 22, 15, 4)),
// bottom
new DisplayCommand.SolidColor(black, new Rect(16, 40, 15, 5)),
// children background
new DisplayCommand.SolidColor(green, new Rect(22, 32, 1, 1)),
// children left
new DisplayCommand.SolidColor(red, new Rect(22, 32, 0, 1)),
// children right
new DisplayCommand.SolidColor(red, new Rect(23, 32, 0, 1)),
// children top
new DisplayCommand.SolidColor(red, new Rect(22, 32, 1, 0)),
// children bottom
new DisplayCommand.SolidColor(red, new Rect(22, 33, 1, 0))
]);
});
export function renderLayoutBox(list: DisplayList, layoutBox: LayoutBox) {
renderBackground(list, layoutBox);
renderBorders(list, layoutBox);
for (let child of layoutBox.children) {
renderLayoutBox(list, child);
}
}
buildDisplayList(layoutRoot: LayoutBox): DisplayList
finally, I got DisplayList from LayoutTree.
test("build display list", () => {
expect(
buildDisplayList(
new LayoutBox(
exampleDimensions,
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([
["border-color", new CssValue.ColorValue(black)],
["background", new CssValue.ColorValue(blue)]
]),
[]
)
),
[]
)
)
).toEqual([
// background
new DisplayCommand.SolidColor(blue, new Rect(16, 22, 15, 23)),
// left
new DisplayCommand.SolidColor(black, new Rect(16, 22, 2, 23)),
// right
new DisplayCommand.SolidColor(black, new Rect(28, 22, 3, 23)),
// top
new DisplayCommand.SolidColor(black, new Rect(16, 22, 15, 4)),
// bottom
new DisplayCommand.SolidColor(black, new Rect(16, 40, 15, 5))
]);
});
test("build display list2", () => {
expect(
buildDisplayList(
new LayoutBox(
new Dimensions(
new Rect(3, 3, 2, 1),
new EdgeSizes(1, 1, 1, 1),
new EdgeSizes(1, 1, 1, 1),
new EdgeSizes(1, 1, 1, 1)
),
new BoxType.BlockNode(
new StyledNode(
new DomNode(),
new Map([
["border-color", new CssValue.ColorValue(red)],
["background", new CssValue.ColorValue(green)]
]),
[]
)
),
[]
)
)
).toEqual([
// background
new DisplayCommand.SolidColor(green, new Rect(1, 1, 6, 5)),
// left
new DisplayCommand.SolidColor(red, new Rect(1, 1, 1, 5)),
// right
new DisplayCommand.SolidColor(red, new Rect(6, 1, 1, 5)),
// top
new DisplayCommand.SolidColor(red, new Rect(1, 1, 6, 1)),
// bottom
new DisplayCommand.SolidColor(red, new Rect(1, 5, 6, 1))
]);
});
export function buildDisplayList(layoutRoot: LayoutBox): DisplayList {
const list: DisplayCommand[] = [];
renderLayoutBox(list, layoutRoot);
return list;
}
🎉
references
- Let's build a browser engine! Part 1: Getting started
- mbrubeck/robinson
- sanemat/js-toy-engine
- sanemat/ts-toy-engine
series
- Let's build browser engine! in typescript vol0 Toy browser engine
- Let's build browser engine! in typescript vol1 Canvas with Color
- Let's build browser engine! in typescript vol2 Display Command
- Let's build browser engine! in typescript vol3 Layout Box, Dimensions
- Let's build browser engine! in typescript vol4 Layout Tree to Display List
💖 💪 🙅 🚩
Murahashi [Matt] Kenichi
Posted on April 30, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
webdev Understanding HTTP, Cookies, Email Protocols, and DNS: A Guide to Key Internet Technologies
November 30, 2024
react Axios NPM Package: A Beginner's Guide to Installing and Making HTTP Requests
November 30, 2024
webdev Guide to Cookies, Local Storage, Session Storage, and Other Web Storage Mechanisms
November 30, 2024