finish textarea rendering

This commit is contained in:
Stefan Niederhauser 2020-02-17 22:48:05 +01:00
parent 3da0ef0ea7
commit 95ca5a627a
4 changed files with 129 additions and 82 deletions

View File

@ -0,0 +1,57 @@
import {strictEqual} from 'assert';
import {layout} from '../textarea-layout';
import {fromCodePoint, toCodePoints} from 'css-line-break';
describe('textarea-layout', () => {
it('should wrap lines at word boundaries', () => {
layoutEqual(' A long text with several lines.',
[' A long text', 'with several', 'lines.']);
});
it('should omit spaces at the end of a line', () => {
layoutEqual(' A long text with several lines. 2',
[' A long text', 'with several', 'lines. 2']);
});
it('should omit spaces at the end of a line 2', () => {
layoutEqual(' A long text with several lines. 2',
[' A long', 'text with', 'several', 'lines. 2']);
});
it('should omit spaces at the end of a line 3', () => {
layoutEqual('A long text with sev eral lines.',
['A long text', 'with sev', 'eral lines.']);
});
it('should respect newlines', () => {
layoutEqual(' A long text\n with\n\n several lines.',
[' A long text', ' with', '', ' several', 'lines.']);
});
it('should wrap too long words', () => {
layoutEqual('Donaudampfschifffahrtskapitänskoch 2',
['Donaudampfsc', 'hifffahrtska', 'pitänskoch 2']);
});
it('should wrap too long words 2', () => {
layoutEqual('Donaudampfschifffahrtskapitänskoch\n 2',
['Donaudampfsc', 'hifffahrtska', 'pitänskoch', ' 2']);
});
it('should wrap too long words 3', () => {
layoutEqual('Donaudampfsc x2',
['Donaudampfsc', 'x2']);
});
});
function layoutEqual(s: string, lines: string[]) {
const pos = layout(toCodePoints(s).map(i => fromCodePoint(i)), 120, _ => 10);
let line = '';
let y = 0;
let j = 0;
for (let i = 0; i < pos.length; i++) {
if (y === pos[i][1]) {
if (pos[i][0] >= 0) {
line += s.charAt(i);
}
} else {
strictEqual(line, lines[j]);
j++;
line = pos[i][0] >= 0 ? s.charAt(i) : '';
y = pos[i][1];
}
}
}

View File

@ -38,6 +38,7 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont
import {SelectElementContainer} from '../../dom/elements/select-element-container';
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
import {TextShadow} from '../../css/property-descriptors/text-shadow';
import {layout} from './textarea-layout';
export type RenderConfigurations = RenderOptions & {
backgroundColor: Color | null;
@ -141,77 +142,18 @@ export class CanvasRenderer {
}
renderTextWithLetterSpacing(text: TextBounds, letterSpacing: number, lineHeight?: number) {
if (letterSpacing === 0) {
if (lineHeight===undefined){
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
}else {
let pos = 0
let word = 0
let line = 0
let y = text.bounds.top + lineHeight / 2
while (pos < text.text.length) {
let c = text.text.charAt(pos)
pos++
switch (c) {
case ' ':
if (this.ctx.measureText(text.text.substring(line, pos - 1)).width > text.bounds.width) {
if (line === word) {
let p = pos
while (this.ctx.measureText(text.text.substring(line, p)).width > text.bounds.width) {
p--
}
this.ctx.fillText(text.text.substring(line, p), text.bounds.left, y)
y += lineHeight
line = word = p
} else {
this.ctx.fillText(text.text.substring(line, word), text.bounds.left, y)
y += lineHeight
let s = word === pos - 1
while (pos < text.text.length && text.text.charAt(pos) === ' ') pos++
line = s ? pos : word
word = pos
}
} else {
word = pos
}
break
case '\n':
if (this.ctx.measureText(text.text.substring(line, pos - 1)).width > text.bounds.width) {
if (line === word) {
let p = pos
while (this.ctx.measureText(text.text.substring(line, p)).width > text.bounds.width) {
p--
}
this.ctx.fillText(text.text.substring(line, p), text.bounds.left, y)
y += lineHeight
line = word = p
} else {
this.ctx.fillText(text.text.substring(line, word), text.bounds.left, y)
y += lineHeight
line = word
word = pos
}
} else {
this.ctx.fillText(text.text.substring(line, pos - 1), text.bounds.left, y)
y += lineHeight
line = pos
word = pos
}
break
default:
}
}
if (pos > line) {
this.ctx.fillText(text.text.substring(line, pos + 1), text.bounds.left, y)
if (lineHeight === undefined) {
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
} else {
const chars = toCodePoints(text.text).map(i => fromCodePoint(i));
const pos = layout(chars, text.bounds.width, s => this.ctx.measureText(s).width + letterSpacing);
const dx = text.bounds.left;
const dy = text.bounds.top + lineHeight / 2;
for (let i = 0; i < pos.length; i++) {
if (pos[i][0] >= 0) {
this.ctx.fillText(chars[i], pos[i][0] + dx, pos[i][1] * lineHeight + dy);
}
}
} else {
const letters = toCodePoints(text.text).map(i => fromCodePoint(i));
letters.reduce((left, letter) => {
this.ctx.fillText(letter, left, text.bounds.top + text.bounds.height);
return left + this.ctx.measureText(letter).width;
}, text.bounds.left);
}
}
@ -455,7 +397,8 @@ export class CanvasRenderer {
]);
this.ctx.clip();
this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number));
const lineHeight = (container instanceof TextareaElementContainer) ? computeLineHeight(styles.lineHeight, styles.fontSize.number) : undefined;
this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, lineHeight);
this.ctx.restore();
this.ctx.textBaseline = 'bottom';
this.ctx.textAlign = 'left';

View File

@ -0,0 +1,45 @@
export function layout(chars: string[], width: number, measure: (s: string) => number): number[][] {
const pos: number[][] = [];
let line = 0;
let lineWidth = 0;
for (let i = 0; i < chars.length; i++) {
if (chars[i] === '\n') {
pos.push([-1, line]);
line++;
lineWidth = 0;
} else {
pos.push([lineWidth, line]);
lineWidth += measure(chars[i]);
if (lineWidth > width) {
if (chars[i] === ' ') {
let p = i;
while (p > 0 && pos[p][1] === line && chars[p] === ' ') p--;
for (let j = i; j > p; j--) {
pos[j][0] = -1;
}
line++;
lineWidth = 0;
while (i < chars.length + 1 && chars[i + 1] === ' ') {
pos.push([-1, line]);
i++;
}
} else {
let p = i;
while (p > 0 && pos[p][1] === line && chars[p] !== ' ') p--;
line++;
if (chars[p] === ' ') {
lineWidth -= pos[p + 1][0];
for (let j = i; j > p; j--) {
pos[j] = [pos[j][0] - pos[p + 1][0], line];
}
pos[p][0] = -1;
} else {
lineWidth -= pos[i][0];
pos[i] = [0, line];
}
}
}
}
}
return pos;
}

View File

@ -16,31 +16,33 @@
</head>
<body>
<div>
<p>1. <code>word wrap</code></p>
<p>1. word wrap</p>
<textarea> A long text with many words that should be wrapped on various lines.
</textarea>
<p>1. <code>word wrap</code></p>
<p>2. padding</p>
<textarea style="padding: 1em">A long text with many words that should be wrapped on various lines.
</textarea>
<p>1. <code>word wrap</code></p>
<p>3. line height</p>
<textarea style="line-height: 25px">A long text with many words.
</textarea>
<p>1. <code>word wrap</code></p>
<p>4. letter spacing</p>
<textarea style="letter-spacing: 5px"> A long text with many words that should be wrapped on various lines.</textarea>
<p>5. multiple spaces</p>
<textarea>A long text with many words that should be wrapped on various lines.
</textarea>
<p>1. <code>word wrap</code></p>
<p>6. newlines</p>
<textarea>A long text with many
words that
should be wrapped on various lines.
</textarea>
<p>1. <code>word wrap</code></p>
<textarea>Donaudampfschifffahrtsgesellschaft
</textarea>
<p>1. <code>word wrap</code></p>
<textarea>Donaudampfschifffahrtsgesellschaft Kapitän Koch
</textarea>
<p>1. <code>word wrap</code></p>
<p>7. long word</p>
<textarea>Donaudampfschifffahrtsgesellschaftskapitänskochmütze</textarea>
</div>