Compare commits

...

9 Commits

Author SHA1 Message Date
Aakansha Doshi
4ed3a2e7be lint 2023-06-07 15:30:57 +05:30
Aakansha Doshi
faec098e30 fix offset in editor 2023-06-07 15:27:39 +05:30
Aakansha Doshi
65e849804d Merge remote-tracking branch 'origin/master' into aakansha-no-trailing-space-render-text-container 2023-06-06 15:07:42 +05:30
Aakansha Doshi
7c0f783cbc Merge remote-tracking branch 'origin/master' into aakansha-no-trailing-space-render-text-container 2023-06-06 13:27:27 +05:30
Aakansha Doshi
fd379c2897 add comment 2023-04-21 13:20:18 +05:30
Aakansha Doshi
97929c07d6 fix tests 2023-04-21 12:20:01 +05:30
Aakansha Doshi
ba22646c22 fix 2023-04-20 21:48:51 +05:30
Aakansha Doshi
c21fecde40 fix element width 2023-04-20 21:45:59 +05:30
Aakansha Doshi
caf0a904db fix: preserve trailing spaces in word wrap 2023-04-20 20:13:01 +05:30
5 changed files with 79 additions and 60 deletions

View File

@ -49,18 +49,7 @@ describe("Test wrapText", () => {
{ {
desc: "break all characters when width of each character is less than container width", desc: "break all characters when width of each character is less than container width",
width: 25, width: 25,
res: `H res: `H\ne\nl\nl\no \nw\nh\na\nt\ns \nu\np`,
e
l
l
o
w
h
a
t
s
u
p`,
}, },
{ {
desc: "break words as per the width", desc: "break words as per the width",
@ -90,8 +79,7 @@ up`,
}); });
describe("When text contain new lines", () => { describe("When text contain new lines", () => {
const text = `Hello const text = "Hello\nwhats up";
whats up`;
[ [
{ {
desc: "break all words when width of each word is less than container width", desc: "break all words when width of each word is less than container width",
@ -101,18 +89,7 @@ whats up`;
{ {
desc: "break all characters when width of each character is less than container width", desc: "break all characters when width of each character is less than container width",
width: 25, width: 25,
res: `H res: `H\ne\nl\nl\no\nw\nh\na\nt\ns \nu\np`,
e
l
l
o
w
h
a
t
s
u
p`,
}, },
{ {
desc: "break words as per the width", desc: "break words as per the width",
@ -149,13 +126,7 @@ whats up`,
desc: "fit characters of long string as per container width and break words as per the width", desc: "fit characters of long string as per container width and break words as per the width",
width: 130, width: 130,
res: `hellolongte res: `hellolongte\nxtthisiswha\ntsupwithyou\nIamtypinggg\nggandtyping\ngg break it \nnow`,
xtthisiswha
tsupwithyou
Iamtypinggg
ggandtyping
gg break it
now`,
}, },
{ {
desc: "fit the long text when container width is greater than text length and move the rest to next line", desc: "fit the long text when container width is greater than text length and move the rest to next line",
@ -190,7 +161,7 @@ now`,
"Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects"; "Wikipedia is hosted by Wikimedia- Foundation, a non-profit organization that also hosts a range-of other projects";
const res = wrapText(text, font, 110); const res = wrapText(text, font, 110);
expect(res).toBe( expect(res).toBe(
`Wikipedia \nis hosted \nby \nWikimedia-\nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts\na range-of\nother \nprojects`, `Wikipedia \nis hosted \nby \nWikimedia- \nFoundation,\na non-\nprofit \norganizati\non that \nalso hosts \na range-of \nother \nprojects`,
); );
text = "Hello thereusing-now"; text = "Hello thereusing-now";

View File

@ -76,6 +76,7 @@ export const redrawTextBoundingBox = (
boundTextUpdates.text, boundTextUpdates.text,
getFontString(textElement), getFontString(textElement),
textElement.lineHeight, textElement.lineHeight,
maxWidth,
); );
boundTextUpdates.width = metrics.width; boundTextUpdates.width = metrics.width;
@ -195,6 +196,7 @@ export const handleBindTextResize = (
text, text,
getFontString(textElement), getFontString(textElement),
textElement.lineHeight, textElement.lineHeight,
maxWidth,
); );
nextHeight = metrics.height; nextHeight = metrics.height;
nextWidth = metrics.width; nextWidth = metrics.width;
@ -283,6 +285,7 @@ export const measureText = (
text: string, text: string,
font: FontString, font: FontString,
lineHeight: ExcalidrawTextElement["lineHeight"], lineHeight: ExcalidrawTextElement["lineHeight"],
maxWidth?: number | null,
) => { ) => {
text = text text = text
.split("\n") .split("\n")
@ -292,7 +295,14 @@ export const measureText = (
.join("\n"); .join("\n");
const fontSize = parseFloat(font); const fontSize = parseFloat(font);
const height = getTextHeight(text, fontSize, lineHeight); const height = getTextHeight(text, fontSize, lineHeight);
const width = getTextWidth(text, font); let width = getTextWidth(text, font);
// Since we now preserve trailing whitespaces so if the text has
// trailing whitespaces, it will be considered in the width and thus width
// computed might be much higher than the allowed max width
// by the container hence making sure the width never goes beyond the max width.
if (maxWidth) {
width = Math.min(width, maxWidth);
}
const baseline = measureBaseline(text, font, lineHeight); const baseline = measureBaseline(text, font, lineHeight);
return { width, height, baseline }; return { width, height, baseline };
}; };
@ -380,7 +390,7 @@ export const getApproxMinLineHeight = (
let canvas: HTMLCanvasElement | undefined; let canvas: HTMLCanvasElement | undefined;
const getLineWidth = (text: string, font: FontString) => { export const getLineWidth = (text: string, font: FontString) => {
if (!canvas) { if (!canvas) {
canvas = document.createElement("canvas"); canvas = document.createElement("canvas");
} }
@ -440,10 +450,8 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
if (!Number.isFinite(maxWidth) || maxWidth < 0) { if (!Number.isFinite(maxWidth) || maxWidth < 0) {
return text; return text;
} }
const lines: Array<string> = []; const lines: Array<string> = [];
const originalLines = text.split("\n"); const originalLines = text.split("\n");
const spaceWidth = getLineWidth(" ", font);
let currentLine = ""; let currentLine = "";
let currentLineWidthTillNow = 0; let currentLineWidthTillNow = 0;
@ -459,7 +467,7 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
currentLineWidthTillNow = 0; currentLineWidthTillNow = 0;
}; };
originalLines.forEach((originalLine) => { originalLines.forEach((originalLine) => {
const currentLineWidth = getTextWidth(originalLine, font); const currentLineWidth = getLineWidth(originalLine, font);
// Push the line if its <= maxWidth // Push the line if its <= maxWidth
if (currentLineWidth <= maxWidth) { if (currentLineWidth <= maxWidth) {
@ -507,23 +515,25 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
} }
} }
// push current line if appending space exceeds max width // push current line if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) { if (currentLineWidthTillNow >= maxWidth) {
push(currentLine); push(currentLine);
resetParams(); resetParams();
// space needs to be appended before next word // space needs to be appended before next word
// as currentLine contains chars which couldn't be appended // as currentLine contains chars which couldn't be appended
// to previous line unless the line ends with hyphen to sync // to previous line unless the line ends with hyphen to sync
// with css word-wrap // with css word-wrap
} else if (!currentLine.endsWith("-")) { } else if (!currentLine.endsWith("-") && index < words.length) {
currentLine += " "; currentLine += " ";
currentLineWidthTillNow += spaceWidth;
} }
index++; index++;
} else { } else {
// Start appending words in a line till max width reached // Start appending words in a line till max width reached
while (currentLineWidthTillNow < maxWidth && index < words.length) { while (currentLineWidthTillNow < maxWidth && index < words.length) {
const word = words[index]; const word = words[index];
currentLineWidthTillNow = getLineWidth(currentLine + word, font); currentLineWidthTillNow = getLineWidth(
`${currentLine + word}`.trimEnd(),
font,
);
if (currentLineWidthTillNow > maxWidth) { if (currentLineWidthTillNow > maxWidth) {
push(currentLine); push(currentLine);
@ -531,24 +541,20 @@ export const wrapText = (text: string, font: FontString, maxWidth: number) => {
break; break;
} }
index++;
// if word ends with "-" then we don't need to add space // if word ends with "-" then we don't need to add space
// to sync with css word-wrap // to sync with css word-wrap
const shouldAppendSpace = !word.endsWith("-"); const shouldAppendSpace = !word.endsWith("-");
currentLine += word; currentLine += word;
if (shouldAppendSpace) { if (shouldAppendSpace && index < words.length) {
currentLine += " "; currentLine += " ";
} }
index++;
// Push the word if appending space exceeds max width // Push the word if appending space exceeds max width
if (currentLineWidthTillNow + spaceWidth >= maxWidth) { if (currentLineWidthTillNow >= maxWidth) {
if (shouldAppendSpace) { lines.push(currentLine);
lines.push(currentLine.slice(0, -1));
} else {
lines.push(currentLine);
}
resetParams(); resetParams();
break; break;
} }
@ -971,3 +977,22 @@ export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
} }
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY]; return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
}; };
export const getSpacesOffsetForLine = (
element: ExcalidrawTextElement,
line: string,
font: FontString,
) => {
const container = getContainerElement(element);
const trailingSpacesWidth =
getLineWidth(line, font) - getLineWidth(line.trimEnd(), font);
const maxWidth = container ? getBoundTextMaxWidth(container) : element.width;
const availableWidth = maxWidth - getLineWidth(line.trimEnd(), font);
let spacesOffset = 0;
if (element.textAlign === TEXT_ALIGN.CENTER) {
spacesOffset = -Math.min(trailingSpacesWidth / 2, availableWidth / 2);
} else if (element.textAlign === TEXT_ALIGN.RIGHT) {
spacesOffset = -Math.min(availableWidth, trailingSpacesWidth);
}
return spacesOffset;
};

View File

@ -11,7 +11,7 @@ import {
isBoundToContainer, isBoundToContainer,
isTextElement, isTextElement,
} from "./typeChecks"; } from "./typeChecks";
import { CLASSES, isSafari } from "../constants"; import { CLASSES, TEXT_ALIGN, isSafari } from "../constants";
import { import {
ExcalidrawElement, ExcalidrawElement,
ExcalidrawLinearElement, ExcalidrawLinearElement,
@ -26,7 +26,7 @@ import {
getContainerDims, getContainerDims,
getContainerElement, getContainerElement,
getTextElementAngle, getTextElementAngle,
getTextWidth, measureText,
normalizeText, normalizeText,
redrawTextBoundingBox, redrawTextBoundingBox,
wrapText, wrapText,
@ -196,6 +196,8 @@ export const textWysiwyg = ({
} }
maxWidth = getBoundTextMaxWidth(container); maxWidth = getBoundTextMaxWidth(container);
textElementWidth = Math.min(textElementWidth, maxWidth);
maxHeight = getBoundTextMaxHeight( maxHeight = getBoundTextMaxHeight(
container, container,
updatedTextElement as ExcalidrawTextElementWithContainer, updatedTextElement as ExcalidrawTextElementWithContainer,
@ -230,7 +232,16 @@ export const textWysiwyg = ({
coordY = y; coordY = y;
} }
} }
const [viewportX, viewportY] = getViewportCoords(coordX, coordY); let spacesOffset = 0;
if (updatedTextElement.textAlign === TEXT_ALIGN.CENTER) {
spacesOffset = Math.max(0, updatedTextElement.width / 2 - maxWidth / 2);
} else if (updatedTextElement.textAlign === TEXT_ALIGN.RIGHT) {
spacesOffset = Math.max(0, updatedTextElement.width - maxWidth);
}
const [viewportX, viewportY] = getViewportCoords(
coordX + spacesOffset,
coordY,
);
const initialSelectionStart = editable.selectionStart; const initialSelectionStart = editable.selectionStart;
const initialSelectionEnd = editable.selectionEnd; const initialSelectionEnd = editable.selectionEnd;
const initialLength = editable.value.length; const initialLength = editable.value.length;
@ -362,7 +373,12 @@ export const textWysiwyg = ({
font, font,
getBoundTextMaxWidth(container), getBoundTextMaxWidth(container),
); );
const width = getTextWidth(wrappedText, font); const { width } = measureText(
wrappedText,
font,
element.lineHeight,
getBoundTextMaxWidth(container),
);
editable.style.width = `${width}px`; editable.style.width = `${width}px`;
} }
}; };

View File

@ -46,6 +46,7 @@ import {
getLineHeightInPx, getLineHeightInPx,
getBoundTextMaxHeight, getBoundTextMaxHeight,
getBoundTextMaxWidth, getBoundTextMaxWidth,
getSpacesOffsetForLine,
} from "../element/textElement"; } from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
@ -319,13 +320,13 @@ const drawElementOnCanvas = (
} }
context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr"); context.canvas.setAttribute("dir", rtl ? "rtl" : "ltr");
context.save(); context.save();
context.font = getFontString(element); const font = getFontString(element);
context.font = font;
context.fillStyle = element.strokeColor; context.fillStyle = element.strokeColor;
context.textAlign = element.textAlign as CanvasTextAlign; context.textAlign = element.textAlign as CanvasTextAlign;
// Canvas does not support multiline text by default // Canvas does not support multiline text by default
const lines = element.text.replace(/\r\n?/g, "\n").split("\n"); const lines = element.text.replace(/\r\n?/g, "\n").split("\n");
const horizontalOffset = const horizontalOffset =
element.textAlign === "center" element.textAlign === "center"
? element.width / 2 ? element.width / 2
@ -336,11 +337,17 @@ const drawElementOnCanvas = (
element.fontSize, element.fontSize,
element.lineHeight, element.lineHeight,
); );
const verticalOffset = element.height - element.baseline; const verticalOffset = element.height - element.baseline;
for (let index = 0; index < lines.length; index++) { for (let index = 0; index < lines.length; index++) {
context.fillText( const spacesOffset = getSpacesOffsetForLine(
element,
lines[index], lines[index],
horizontalOffset, font,
);
context.fillText(
lines[index].trimEnd(),
horizontalOffset + spacesOffset,
(index + 1) * lineHeightPx - verticalOffset, (index + 1) * lineHeightPx - verticalOffset,
); );
} }

View File

@ -1151,7 +1151,7 @@ describe("Test Linear Elements", () => {
expect( expect(
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
"Online whiteboard collaboration "Online whiteboard collaboration
made easy" made easy"
`); `);
const handleBindTextResizeSpy = jest.spyOn( const handleBindTextResizeSpy = jest.spyOn(