1
0
mirror of https://github.com/excalidraw/excalidraw.git synced 2024-11-02 03:25:53 +01:00

fix: use canvas measureText to calculate width in measureText (#6030)

* fix: use canvas measureText to calculate width in measureText

* calculate multiline width correctly using canvas measure text and rename functions

* set correct width when pasting in bound container

* take existing value + new pasted

* remove debugger :p

* fix snaps
This commit is contained in:
Aakansha Doshi 2022-12-23 21:45:49 +05:30 committed by GitHub
parent 2595e0de82
commit af3b93c410
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 16 deletions

@ -157,7 +157,7 @@ describe("Test measureText", () => {
expect(res.container).toMatchInlineSnapshot(`
<div
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; width: 191px; overflow: hidden; word-break: break-word; line-height: 0px;"
style="position: absolute; white-space: pre-wrap; font: Emoji 20px 20px; min-height: 1em; width: 111px; overflow: hidden; word-break: break-word; line-height: 0px;"
>
<span
style="display: inline-block; overflow: hidden; width: 1px; height: 1px;"

@ -271,9 +271,12 @@ export const measureText = (
container.style.whiteSpace = "pre";
container.style.font = font;
container.style.minHeight = "1em";
const textWidth = getTextWidth(text, font);
if (maxWidth) {
const lineHeight = getApproxLineHeight(font);
container.style.width = `${String(maxWidth + 1)}px`;
container.style.width = `${String(Math.min(textWidth, maxWidth) + 1)}px`;
container.style.overflow = "hidden";
container.style.wordBreak = "break-word";
container.style.lineHeight = `${String(lineHeight)}px`;
@ -291,7 +294,10 @@ export const measureText = (
// Baseline is important for positioning text on canvas
const baseline = span.offsetTop + span.offsetHeight;
// Since span adds 1px extra width to the container
const width = container.offsetWidth - 1;
let width = container.offsetWidth;
if (maxWidth && textWidth > maxWidth) {
width = width - 1;
}
const height = container.offsetHeight;
document.body.removeChild(container);
if (isTestEnv()) {
@ -312,7 +318,7 @@ export const getApproxLineHeight = (font: FontString) => {
};
let canvas: HTMLCanvasElement | undefined;
const getTextWidth = (text: string, font: FontString) => {
const getLineWidth = (text: string, font: FontString) => {
if (!canvas) {
canvas = document.createElement("canvas");
}
@ -330,10 +336,18 @@ const getTextWidth = (text: string, font: FontString) => {
return metrics.width;
};
export const getTextWidth = (text: string, font: FontString) => {
const lines = text.split("\n");
let width = 0;
lines.forEach((line) => {
width = Math.max(width, getLineWidth(line, font));
});
return width;
};
export const wrapText = (text: string, font: FontString, maxWidth: number) => {
const lines: Array<string> = [];
const originalLines = text.split("\n");
const spaceWidth = getTextWidth(" ", font);
const spaceWidth = getLineWidth(" ", font);
const push = (str: string) => {
if (str.trim()) {
@ -351,7 +365,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
let index = 0;
while (index < words.length) {
const currentWordWidth = getTextWidth(words[index], font);
const currentWordWidth = getLineWidth(words[index], font);
// Start breaking longer words exceeding max width
if (currentWordWidth >= maxWidth) {
@ -400,7 +414,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
// Start appending words in a line till max width reached
while (currentLineWidthTillNow < maxWidth && index < words.length) {
const word = words[index];
currentLineWidthTillNow = getTextWidth(currentLine + word, font);
currentLineWidthTillNow = getLineWidth(currentLine + word, font);
if (currentLineWidthTillNow >= maxWidth) {
push(currentLine);
@ -448,7 +462,7 @@ export const charWidth = (() => {
cachedCharWidth[font] = [];
}
if (!cachedCharWidth[font][ascii]) {
const width = getTextWidth(char, font);
const width = getLineWidth(char, font);
cachedCharWidth[font][ascii] = width;
}
@ -508,7 +522,7 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
while (widthTillNow <= width) {
const batch = dummyText.substr(index, index + batchLength);
str += batch;
widthTillNow += getTextWidth(str, font);
widthTillNow += getLineWidth(str, font);
if (index === dummyText.length - 1) {
index = 0;
}
@ -517,7 +531,7 @@ export const getApproxCharsToFitInWidth = (font: FontString, width: number) => {
while (widthTillNow > width) {
str = str.substr(0, str.length - 1);
widthTillNow = getTextWidth(str, font);
widthTillNow = getLineWidth(str, font);
}
return str.length;
};

@ -862,7 +862,7 @@ describe("textWysiwyg", () => {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
110.5,
110,
17,
]
`);
@ -910,7 +910,7 @@ describe("textWysiwyg", () => {
resize(rectangle, "ne", [rectangle.x + 100, rectangle.y - 100]);
expect([h.elements[1].x, h.elements[1].y]).toMatchInlineSnapshot(`
Array [
426,
425,
-539,
]
`);
@ -1026,7 +1026,7 @@ describe("textWysiwyg", () => {
mouse.up(rectangle.x + 100, rectangle.y + 50);
expect(rectangle.x).toBe(80);
expect(rectangle.y).toBe(85);
expect(text.x).toBe(90.5);
expect(text.x).toBe(90);
expect(text.y).toBe(90);
Keyboard.withModifierKeys({ ctrl: true }, () => {

@ -28,6 +28,7 @@ import {
getContainerDims,
getContainerElement,
getTextElementAngle,
getTextWidth,
normalizeText,
wrapText,
} from "./textElement";
@ -39,6 +40,7 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
import App from "../components/App";
import { getMaxContainerHeight, getMaxContainerWidth } from "./newElement";
import { LinearElementEditor } from "./linearElementEditor";
import { parseClipboard } from "../clipboard";
const getTransform = (
width: number,
@ -348,6 +350,32 @@ export const textWysiwyg = ({
updateWysiwygStyle();
if (onChange) {
editable.onpaste = async (event) => {
const clipboardData = await parseClipboard(event, true);
if (!clipboardData.text) {
return;
}
const data = normalizeText(clipboardData.text);
if (!data) {
return;
}
const container = getContainerElement(element);
const font = getFontString({
fontSize: app.state.currentItemFontSize,
fontFamily: app.state.currentItemFontFamily,
});
if (container) {
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getMaxContainerWidth(container),
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
}
};
editable.oninput = () => {
const updatedTextElement = Scene.getScene(element)?.getElement(
id,

@ -312,7 +312,7 @@ Object {
"versionNonce": 0,
"verticalAlign": "middle",
"width": 100,
"x": 0.5,
"x": 0,
"y": 0,
}
`;

@ -1027,7 +1027,7 @@ describe("Test Linear Elements", () => {
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 387.5,
"x": 387,
"y": 70,
}
`);
@ -1086,7 +1086,7 @@ describe("Test Linear Elements", () => {
expect(getBoundTextElementPosition(container, textElement))
.toMatchInlineSnapshot(`
Object {
"x": 190.5,
"x": 190,
"y": 20,
}
`);