feat: improve text measurements in bound containers (#6187)

* feat: move to canvas measureText

* calcualte height with better heuristic

* improve heuristic more

* remove vertical offset as its not needed

* lint

* calculate width of individual char and ceil to calculate width and remove adjustment factor

* push the word if equal to max width

* update height when text overflows for vertical alignment top/bottom

* remove the hack of updating height when line mismatch as its not needed

* remove scroll height and calculate the height instead

* remove unused code

* fix

* remove

* use math.ceil for whole width instead of individual chars

* fix tests

* fix

* fix

* redraw text bounding box instead when font loaded to fix alignment as well

* fix

* fix

* fix

* Add a 0.05px extra only for firefox

* Add spec

* stop taking ceil and increase firefox editor width by 0.05px

* Ad 0.05px in safari too

* lint

* lint

* remove baseline from measureFontSizeFromWH

* don't redraw on font load

* lint

* refactor name and signature
This commit is contained in:
Aakansha Doshi 2023-02-23 16:33:10 +05:30 committed by GitHub
parent 39b96cb011
commit 9659254fd6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 142 additions and 349 deletions

View file

@ -11,7 +11,7 @@ import {
isBoundToContainer,
isTextElement,
} from "./typeChecks";
import { CLASSES, VERTICAL_ALIGN } from "../constants";
import { CLASSES, isFirefox, isSafari, VERTICAL_ALIGN } from "../constants";
import {
ExcalidrawElement,
ExcalidrawLinearElement,
@ -29,6 +29,7 @@ import {
getContainerElement,
getTextElementAngle,
getTextWidth,
measureText,
normalizeText,
redrawTextBoundingBox,
wrapText,
@ -159,7 +160,7 @@ export const textWysiwyg = ({
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
const width = updatedTextElement.width;
let textElementWidth = updatedTextElement.width;
// Set to element height by default since that's
// what is going to be used for unbounded text
let textElementHeight = updatedTextElement.height;
@ -272,7 +273,10 @@ export const textWysiwyg = ({
if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
}
// As firefox, Safari needs little higher dimensions on DOM
if (isFirefox || isSafari) {
textElementWidth += 0.5;
}
// Make sure text editor height doesn't go beyond viewport
const editorMaxHeight =
(appState.height - viewportY) / appState.zoom.value;
@ -280,12 +284,12 @@ export const textWysiwyg = ({
font: getFontString(updatedTextElement),
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: `${lineHeight}px`,
width: `${Math.min(width, maxWidth)}px`,
width: `${textElementWidth}px`,
height: `${textElementHeight}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: getTransform(
width,
textElementWidth,
textElementHeight,
getTextElementAngle(updatedTextElement),
appState,
@ -378,55 +382,16 @@ export const textWysiwyg = ({
id,
) as ExcalidrawTextElement;
const font = getFontString(updatedTextElement);
// using scrollHeight here since we need to calculate
// number of lines so cannot use editable.style.height
// as that gets updated below
// Rounding here so that the lines calculated is more accurate in all browsers.
// The scrollHeight and approxLineHeight differs in diff browsers
// eg it gives 1.05 in firefox for handewritten small font due to which
// height gets updated as lines > 1 and leads to jumping text for first line in bound container
// hence rounding here to avoid that
const lines = Math.round(
editable.scrollHeight / getApproxLineHeight(font),
);
// auto increase height only when lines > 1 so its
// measured correctly and vertically aligns for
// first line as well as setting height to "auto"
// doubles the height as soon as user starts typing
if (isBoundToContainer(element) && lines > 1) {
if (isBoundToContainer(element)) {
const container = getContainerElement(element);
let height = "auto";
editable.style.height = "0px";
let heightSet = false;
if (lines === 2) {
const actualLineCount = wrapText(
editable.value,
font,
getMaxContainerWidth(container!),
).split("\n").length;
// This is browser behaviour when setting height to "auto"
// It sets the height needed for 2 lines even if actual
// line count is 1 as mentioned above as well
// hence reducing the height by half if actual line count is 1
// so single line aligns vertically when deleting
if (actualLineCount === 1) {
height = `${editable.scrollHeight / 2}px`;
editable.style.height = height;
heightSet = true;
}
}
const wrappedText = wrapText(
normalizeText(editable.value),
font,
getMaxContainerWidth(container!),
);
const width = getTextWidth(wrappedText, font);
const { width, height } = measureText(wrappedText, font);
editable.style.width = `${width}px`;
if (!heightSet) {
editable.style.height = `${editable.scrollHeight}px`;
}
editable.style.height = `${height}px`;
}
onChange(normalizeText(editable.value));
};