Reduce number of forced reflows when rendering

This commit is contained in:
David Evans 2018-02-08 23:13:19 +00:00
parent d283fe158c
commit ab3d67f313
16 changed files with 262 additions and 133 deletions

View File

@ -3387,13 +3387,17 @@ define('svg/SVGUtilities',[],() => {
return document.createTextNode(text);
}
function make(type, attrs = {}, children = []) {
const o = document.createElementNS(NS, type);
function setAttributes(target, attrs) {
for(const k in attrs) {
if(attrs.hasOwnProperty(k)) {
o.setAttribute(k, attrs[k]);
target.setAttribute(k, attrs[k]);
}
}
}
function make(type, attrs = {}, children = []) {
const o = document.createElementNS(NS, type);
setAttributes(o, attrs);
for(const c of children) {
o.appendChild(c);
}
@ -3417,6 +3421,7 @@ define('svg/SVGUtilities',[],() => {
makeText,
make,
makeContainer,
setAttributes,
empty,
};
});
@ -3461,6 +3466,27 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
});
}
function measureLine(tester, line) {
if(!line.length) {
return 0;
}
const labelKey = JSON.stringify(line);
const knownWidth = tester.widths.get(labelKey);
if(knownWidth !== undefined) {
return knownWidth;
}
// getComputedTextLength forces a reflow, so only call it if nothing
// else can tell us the length
svg.empty(tester.node);
populateSvgTextLine(tester.node, line);
const width = tester.node.getComputedTextLength();
tester.widths.set(labelKey, width);
return width;
}
const EMPTY = [];
class SVGTextBlock {
@ -3472,8 +3498,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
x: 0,
y: 0,
};
this.width = 0;
this.height = 0;
this.lines = [];
this.set(initialState);
}
@ -3499,8 +3523,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
_reset() {
this._rebuildLines(0);
this.width = 0;
this.height = 0;
}
_renderText() {
@ -3516,7 +3538,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
this._rebuildLines(formatted.length);
let maxWidth = 0;
this.lines.forEach((ln, i) => {
const id = JSON.stringify(formatted[i]);
if(id !== ln.latest) {
@ -3524,9 +3545,7 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
populateSvgTextLine(ln.node, formatted[i]);
ln.latest = id;
}
maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength());
});
this.width = maxWidth;
}
_updateX() {
@ -3540,7 +3559,6 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
this.lines.forEach(({node}, i) => {
node.setAttribute('y', this.state.y + i * lineHeight + size);
});
this.height = lineHeight * this.lines.length;
}
firstLine() {
@ -3587,47 +3605,62 @@ define('svg/SVGTextBlock',['./SVGUtilities'], (svg) => {
this.cache = new Map();
}
measure(attrs, formatted) {
if(!formatted || !formatted.length) {
return {width: 0, height: 0};
}
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
_measureHeight({attrs, formatted}) {
return formatted.length * fontDetails(attrs).lineHeight;
}
let tester = this.cache.get(attrs);
_measureWidth({attrs, formatted}) {
if(!formatted.length) {
return 0;
}
const attrKey = JSON.stringify(attrs);
let tester = this.cache.get(attrKey);
if(!tester) {
tester = svg.make('text', attrs);
this.testers.appendChild(tester);
this.cache.set(attrs, tester);
const node = svg.make('text', attrs);
this.testers.appendChild(node);
tester = {
node,
widths: new Map(),
};
this.cache.set(attrKey, tester);
}
if(!this.testers.parentNode) {
this.container.appendChild(this.testers);
}
let width = 0;
formatted.forEach((line) => {
svg.empty(tester);
populateSvgTextLine(tester, line);
width = Math.max(width, tester.getComputedTextLength());
});
return (formatted
.map((line) => measureLine(tester, line))
.reduce((a, b) => Math.max(a, b), 0)
);
}
_getMeasurementOpts(attrs, formatted) {
if(!formatted) {
if(typeof attrs === 'object' && attrs.state) {
formatted = attrs.state.formatted || [];
attrs = attrs.state.attrs;
} else {
formatted = [];
}
} else if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
return {attrs, formatted};
}
measure(attrs, formatted) {
const opts = this._getMeasurementOpts(attrs, formatted);
return {
width,
height: formatted.length * fontDetails(attrs).lineHeight,
width: this._measureWidth(opts),
height: this._measureHeight(opts),
};
}
measureHeight(attrs, formatted) {
if(!formatted) {
return 0;
}
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
return formatted.length * fontDetails(attrs).lineHeight;
const opts = this._getMeasurementOpts(attrs, formatted);
return this._measureHeight(opts);
}
resetCache() {
@ -3838,6 +3871,7 @@ define('svg/SVGShapes',[
labelLayer,
boxRenderer = null,
SVGTextBlockClass = SVGTextBlock,
textSizer,
}) {
if(!formatted || !formatted.length) {
return {width: 0, height: 0, label: null, box: null};
@ -3852,20 +3886,21 @@ define('svg/SVGShapes',[
y: y + padding.top,
});
const width = (label.width + padding.left + padding.right);
const height = (label.height + padding.top + padding.bottom);
const size = textSizer.measure(label);
const width = (size.width + padding.left + padding.right);
const height = (size.height + padding.top + padding.bottom);
let box = null;
if(boxRenderer) {
box = boxRenderer({
'x': anchorX - label.width * shift - padding.left,
'x': anchorX - size.width * shift - padding.left,
'y': y,
'width': width,
'height': height,
});
} else {
box = renderBox(boxAttrs, {
'x': anchorX - label.width * shift - padding.left,
'x': anchorX - size.width * shift - padding.left,
'y': y,
'width': width,
'height': height,
@ -4064,6 +4099,7 @@ define('sequence/components/Block',[
boxLayer: blockInfo.hold,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
const labelRender = SVGShapes.renderBoxedText(label, {
@ -4075,6 +4111,7 @@ define('sequence/components/Block',[
boxLayer: env.lineMaskLayer,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
const labelHeight = Math.max(
@ -4434,6 +4471,7 @@ define('sequence/components/AgentCap',[
boxLayer: clickable,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
clickable.insertBefore(svg.make('rect', {
'x': x - text.width / 2,
@ -5003,6 +5041,7 @@ define('sequence/components/Connect',[
boxLayer: env.lineMaskLayer,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
const labelW = (label ? (
renderedText.width +
@ -5129,6 +5168,7 @@ define('sequence/components/Connect',[
boxLayer: env.lineMaskLayer,
labelLayer,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
}
@ -5348,15 +5388,16 @@ define('sequence/components/Note',['./BaseComponent', 'svg/SVGUtilities'], (Base
formatted: label,
y,
});
const size = env.textSizer.measure(labelNode);
const fullW = (
labelNode.width +
size.width +
config.padding.left +
config.padding.right
);
const fullH = (
config.padding.top +
labelNode.height +
size.height +
config.padding.bottom
);
if(x0 === null && xMid !== null) {
@ -5638,6 +5679,7 @@ define('sequence/components/Divider',[
boxLayer: env.fullMaskLayer,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
labelWidth = boxed.width;
}
@ -6171,12 +6213,13 @@ define('sequence/Renderer',[
updateBounds(stagesHeight) {
const cx = (this.minX + this.maxX) / 2;
const titleY = ((this.title.height > 0) ?
(-this.theme.titleMargin - this.title.height) : 0
const titleSize = this.sizer.measure(this.title);
const titleY = ((titleSize.height > 0) ?
(-this.theme.titleMargin - titleSize.height) : 0
);
this.title.set({x: cx, y: titleY});
const halfTitleWidth = this.title.width / 2;
const halfTitleWidth = titleSize.width / 2;
const margin = this.theme.outerMargin;
const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
@ -6186,15 +6229,15 @@ define('sequence/Renderer',[
this.width = x1 - x0;
this.height = y1 - y0;
this.fullMaskReveal.setAttribute('x', x0);
this.fullMaskReveal.setAttribute('y', y0);
this.fullMaskReveal.setAttribute('width', this.width);
this.fullMaskReveal.setAttribute('height', this.height);
const fullSize = {
'x': x0,
'y': y0,
'width': this.width,
'height': this.height,
};
this.lineMaskReveal.setAttribute('x', x0);
this.lineMaskReveal.setAttribute('y', y0);
this.lineMaskReveal.setAttribute('width', this.width);
this.lineMaskReveal.setAttribute('height', this.height);
svg.setAttributes(this.fullMaskReveal, fullSize);
svg.setAttributes(this.lineMaskReveal, fullSize);
this.base.setAttribute('viewBox', (
x0 + ' ' + y0 + ' ' +

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
defineDescribe('Readme', [
define([
'./SequenceDiagram',
'image/ImageRegion',
'image/ImageSimilarity',
@ -70,6 +70,8 @@ defineDescribe('Readme', [
return (fetch('README.md')
.then((response) => response.text())
.then(findSamples)
.then((samples) => samples.forEach(makeSampleTests))
.then((samples) => describe('Readme', () => {
samples.forEach(makeSampleTests);
}))
);
});

View File

@ -494,12 +494,13 @@ define([
updateBounds(stagesHeight) {
const cx = (this.minX + this.maxX) / 2;
const titleY = ((this.title.height > 0) ?
(-this.theme.titleMargin - this.title.height) : 0
const titleSize = this.sizer.measure(this.title);
const titleY = ((titleSize.height > 0) ?
(-this.theme.titleMargin - titleSize.height) : 0
);
this.title.set({x: cx, y: titleY});
const halfTitleWidth = this.title.width / 2;
const halfTitleWidth = titleSize.width / 2;
const margin = this.theme.outerMargin;
const x0 = Math.min(this.minX, cx - halfTitleWidth) - margin;
const x1 = Math.max(this.maxX, cx + halfTitleWidth) + margin;
@ -509,15 +510,15 @@ define([
this.width = x1 - x0;
this.height = y1 - y0;
this.fullMaskReveal.setAttribute('x', x0);
this.fullMaskReveal.setAttribute('y', y0);
this.fullMaskReveal.setAttribute('width', this.width);
this.fullMaskReveal.setAttribute('height', this.height);
const fullSize = {
'x': x0,
'y': y0,
'width': this.width,
'height': this.height,
};
this.lineMaskReveal.setAttribute('x', x0);
this.lineMaskReveal.setAttribute('y', y0);
this.lineMaskReveal.setAttribute('width', this.width);
this.lineMaskReveal.setAttribute('height', this.height);
svg.setAttributes(this.fullMaskReveal, fullSize);
svg.setAttributes(this.lineMaskReveal, fullSize);
this.base.setAttribute('viewBox', (
x0 + ' ' + y0 + ' ' +

View File

@ -153,4 +153,35 @@ defineDescribe('SequenceDiagram', [
expect(content).toContain('<g class="region collapsed"');
});
it('measures OS fonts correctly on the first render', (done) => {
const code = 'title message';
const sd = new SequenceDiagram(code);
const widthImmediate = sd.getSize().width;
expect(widthImmediate).toBeGreaterThan(40);
sd.set(code);
expect(sd.getSize().width).toEqual(widthImmediate);
setTimeout(() => {
sd.set(code);
expect(sd.getSize().width).toEqual(widthImmediate);
done();
}, 500);
});
it('measures embedded fonts correctly on the first render', (done) => {
const code = 'theme sketch\ntitle message';
const sd = new SequenceDiagram(code);
const widthImmediate = sd.getSize().width;
expect(widthImmediate).toBeGreaterThan(40);
sd.set(code);
expect(sd.getSize().width).toEqual(widthImmediate);
setTimeout(() => {
sd.set(code);
expect(sd.getSize().width).toEqual(widthImmediate);
done();
}, 500);
});
});

View File

@ -50,6 +50,7 @@ define([
boxLayer: clickable,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
clickable.insertBefore(svg.make('rect', {
'x': x - text.width / 2,

View File

@ -56,6 +56,7 @@ define([
boxLayer: blockInfo.hold,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
const labelRender = SVGShapes.renderBoxedText(label, {
@ -67,6 +68,7 @@ define([
boxLayer: env.lineMaskLayer,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
const labelHeight = Math.max(

View File

@ -227,6 +227,7 @@ define([
boxLayer: env.lineMaskLayer,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
const labelW = (label ? (
renderedText.width +
@ -353,6 +354,7 @@ define([
boxLayer: env.lineMaskLayer,
labelLayer,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
}

View File

@ -63,6 +63,7 @@ define([
boxLayer: env.fullMaskLayer,
labelLayer: clickable,
SVGTextBlockClass: env.SVGTextBlockClass,
textSizer: env.textSizer,
});
labelWidth = boxed.width;
}

View File

@ -42,15 +42,16 @@ define(['./BaseComponent', 'svg/SVGUtilities'], (BaseComponent, svg) => {
formatted: label,
y,
});
const size = env.textSizer.measure(labelNode);
const fullW = (
labelNode.width +
size.width +
config.padding.left +
config.padding.right
);
const fullH = (
config.padding.top +
labelNode.height +
size.height +
config.padding.bottom
);
if(x0 === null && xMid !== null) {

View File

@ -25,8 +25,6 @@ define(['svg/SVGUtilities'], (svg) => {
x: 0,
y: 0,
};
this.width = 0;
this.height = 0;
this.nodes = [];
this.set(initialState);
}
@ -53,8 +51,6 @@ define(['svg/SVGUtilities'], (svg) => {
_reset() {
this._rebuildNodes(0);
this.width = 0;
this.height = 0;
}
_renderText() {
@ -66,13 +62,10 @@ define(['svg/SVGUtilities'], (svg) => {
const formatted = this.state.formatted;
this._rebuildNodes(formatted.length);
let maxWidth = 0;
this.nodes.forEach(({text, element}, i) => {
const ln = formatted[i].reduce((v, pt) => v + pt.text, '');
text.nodeValue = ln;
maxWidth = Math.max(maxWidth, ln.length);
});
this.width = maxWidth;
}
_updateX() {
@ -85,7 +78,6 @@ define(['svg/SVGUtilities'], (svg) => {
this.nodes.forEach(({element}, i) => {
element.setAttribute('y', this.state.y + i);
});
this.height = this.nodes.length;
}
firstLine() {
@ -123,6 +115,11 @@ define(['svg/SVGUtilities'], (svg) => {
class SizeTester {
measure(attrs, formatted) {
if(attrs.state) {
formatted = attrs.state.formatted;
attrs = attrs.state.attrs;
}
if(!formatted || !formatted.length) {
return {width: 0, height: 0};
}

View File

@ -74,6 +74,7 @@ define([
labelLayer,
boxRenderer = null,
SVGTextBlockClass = SVGTextBlock,
textSizer,
}) {
if(!formatted || !formatted.length) {
return {width: 0, height: 0, label: null, box: null};
@ -88,20 +89,21 @@ define([
y: y + padding.top,
});
const width = (label.width + padding.left + padding.right);
const height = (label.height + padding.top + padding.bottom);
const size = textSizer.measure(label);
const width = (size.width + padding.left + padding.right);
const height = (size.height + padding.top + padding.bottom);
let box = null;
if(boxRenderer) {
box = boxRenderer({
'x': anchorX - label.width * shift - padding.left,
'x': anchorX - size.width * shift - padding.left,
'y': y,
'width': width,
'height': height,
});
} else {
box = renderBox(boxAttrs, {
'x': anchorX - label.width * shift - padding.left,
'x': anchorX - size.width * shift - padding.left,
'y': y,
'width': width,
'height': height,

View File

@ -54,8 +54,17 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
});
describe('renderBoxedText', () => {
let o = null;
let sizer = null;
beforeEach(() => {
o = document.createElement('p');
sizer = {
measure: () => ({width: 64, height: 128}),
};
});
it('renders a label', () => {
const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
x: 1,
y: 2,
@ -64,6 +73,7 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
labelAttrs: {'font-size': 10, 'line-height': 1.5, 'foo': 'bar'},
boxLayer: o,
labelLayer: o,
textSizer: sizer,
});
expect(rendered.label.state.formatted).toEqual([[{text: 'foo'}]]);
expect(rendered.label.state.x).toEqual(5);
@ -72,7 +82,6 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
});
it('positions a box beneath the rendered label', () => {
const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
x: 1,
y: 2,
@ -81,16 +90,19 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
labelAttrs: {'font-size': 10, 'line-height': 1.5},
boxLayer: o,
labelLayer: o,
textSizer: sizer,
});
expect(rendered.box.getAttribute('x')).toEqual('1');
expect(rendered.box.getAttribute('y')).toEqual('2');
expect(rendered.box.getAttribute('height')).toEqual('55');
expect(rendered.box.getAttribute('width'))
.toEqual(String(4 + 16 + 64));
expect(rendered.box.getAttribute('height'))
.toEqual(String(8 + 32 + 128));
expect(rendered.box.getAttribute('foo')).toEqual('bar');
expect(rendered.box.parentNode).toEqual(o);
});
it('returns the size of the rendered box', () => {
const o = document.createElement('p');
const rendered = SVGShapes.renderBoxedText([[{text: 'foo'}]], {
x: 1,
y: 2,
@ -99,9 +111,10 @@ defineDescribe('SVGShapes', ['./SVGShapes'], (SVGShapes) => {
labelAttrs: {'font-size': 10, 'line-height': 1.5},
boxLayer: o,
labelLayer: o,
textSizer: sizer,
});
expect(rendered.width).toBeGreaterThan(20 - 1);
expect(rendered.height).toEqual(55);
expect(rendered.width).toEqual(4 + 16 + 64);
expect(rendered.height).toEqual(8 + 32 + 128);
});
});
});

View File

@ -38,6 +38,27 @@ define(['./SVGUtilities'], (svg) => {
});
}
function measureLine(tester, line) {
if(!line.length) {
return 0;
}
const labelKey = JSON.stringify(line);
const knownWidth = tester.widths.get(labelKey);
if(knownWidth !== undefined) {
return knownWidth;
}
// getComputedTextLength forces a reflow, so only call it if nothing
// else can tell us the length
svg.empty(tester.node);
populateSvgTextLine(tester.node, line);
const width = tester.node.getComputedTextLength();
tester.widths.set(labelKey, width);
return width;
}
const EMPTY = [];
class SVGTextBlock {
@ -49,8 +70,6 @@ define(['./SVGUtilities'], (svg) => {
x: 0,
y: 0,
};
this.width = 0;
this.height = 0;
this.lines = [];
this.set(initialState);
}
@ -76,8 +95,6 @@ define(['./SVGUtilities'], (svg) => {
_reset() {
this._rebuildLines(0);
this.width = 0;
this.height = 0;
}
_renderText() {
@ -93,7 +110,6 @@ define(['./SVGUtilities'], (svg) => {
this._rebuildLines(formatted.length);
let maxWidth = 0;
this.lines.forEach((ln, i) => {
const id = JSON.stringify(formatted[i]);
if(id !== ln.latest) {
@ -101,9 +117,7 @@ define(['./SVGUtilities'], (svg) => {
populateSvgTextLine(ln.node, formatted[i]);
ln.latest = id;
}
maxWidth = Math.max(maxWidth, ln.node.getComputedTextLength());
});
this.width = maxWidth;
}
_updateX() {
@ -117,7 +131,6 @@ define(['./SVGUtilities'], (svg) => {
this.lines.forEach(({node}, i) => {
node.setAttribute('y', this.state.y + i * lineHeight + size);
});
this.height = lineHeight * this.lines.length;
}
firstLine() {
@ -164,47 +177,62 @@ define(['./SVGUtilities'], (svg) => {
this.cache = new Map();
}
measure(attrs, formatted) {
if(!formatted || !formatted.length) {
return {width: 0, height: 0};
}
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
_measureHeight({attrs, formatted}) {
return formatted.length * fontDetails(attrs).lineHeight;
}
let tester = this.cache.get(attrs);
_measureWidth({attrs, formatted}) {
if(!formatted.length) {
return 0;
}
const attrKey = JSON.stringify(attrs);
let tester = this.cache.get(attrKey);
if(!tester) {
tester = svg.make('text', attrs);
this.testers.appendChild(tester);
this.cache.set(attrs, tester);
const node = svg.make('text', attrs);
this.testers.appendChild(node);
tester = {
node,
widths: new Map(),
};
this.cache.set(attrKey, tester);
}
if(!this.testers.parentNode) {
this.container.appendChild(this.testers);
}
let width = 0;
formatted.forEach((line) => {
svg.empty(tester);
populateSvgTextLine(tester, line);
width = Math.max(width, tester.getComputedTextLength());
});
return (formatted
.map((line) => measureLine(tester, line))
.reduce((a, b) => Math.max(a, b), 0)
);
}
_getMeasurementOpts(attrs, formatted) {
if(!formatted) {
if(typeof attrs === 'object' && attrs.state) {
formatted = attrs.state.formatted || [];
attrs = attrs.state.attrs;
} else {
formatted = [];
}
} else if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
return {attrs, formatted};
}
measure(attrs, formatted) {
const opts = this._getMeasurementOpts(attrs, formatted);
return {
width,
height: formatted.length * fontDetails(attrs).lineHeight,
width: this._measureWidth(opts),
height: this._measureHeight(opts),
};
}
measureHeight(attrs, formatted) {
if(!formatted) {
return 0;
}
if(!Array.isArray(formatted)) {
throw new Error('Invalid formatted text: ' + formatted);
}
return formatted.length * fontDetails(attrs).lineHeight;
const opts = this._getMeasurementOpts(attrs, formatted);
return this._measureHeight(opts);
}
resetCache() {

View File

@ -68,12 +68,6 @@ defineDescribe('SVGTextBlock', [
expect(hold.children[1].innerHTML).toEqual('bar');
});
it('populates width and height with the size of the text', () => {
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
expect(block.width).toBeGreaterThan(0);
expect(block.height).toEqual(30);
});
it('re-uses text nodes when possible, adding more if needed', () => {
block.set({formatted: [[{text: 'foo'}], [{text: 'bar'}]]});
const line0 = hold.children[0];
@ -129,8 +123,7 @@ defineDescribe('SVGTextBlock', [
block.set({formatted: []});
expect(hold.children.length).toEqual(0);
expect(block.state.formatted).toEqual([]);
expect(block.width).toEqual(0);
expect(block.height).toEqual(0);
expect(block.lines.length).toEqual(0);
});
});
@ -148,6 +141,13 @@ defineDescribe('SVGTextBlock', [
expect(size.height).toEqual(15);
});
it('calculates the size of text blocks', () => {
block.set({formatted: [[{text: 'foo'}]]});
const size = tester.measure(block);
expect(size.width).toBeGreaterThan(0);
expect(size.height).toEqual(15);
});
it('measures multiline text', () => {
const size = tester.measure(attrs, [
[{text: 'foo'}],

View File

@ -7,13 +7,17 @@ define(() => {
return document.createTextNode(text);
}
function make(type, attrs = {}, children = []) {
const o = document.createElementNS(NS, type);
function setAttributes(target, attrs) {
for(const k in attrs) {
if(attrs.hasOwnProperty(k)) {
o.setAttribute(k, attrs[k]);
target.setAttribute(k, attrs[k]);
}
}
}
function make(type, attrs = {}, children = []) {
const o = document.createElementNS(NS, type);
setAttributes(o, attrs);
for(const c of children) {
o.appendChild(c);
}
@ -37,6 +41,7 @@ define(() => {
makeText,
make,
makeContainer,
setAttributes,
empty,
};
});