refactor: FONT_FAMILY and related helpers

This commit is contained in:
dwelle 2023-08-25 21:05:45 +02:00
parent de1ebad755
commit ffa4cea61c
23 changed files with 130 additions and 115 deletions

View File

@ -10,13 +10,13 @@ import { FONT_FAMILY } from "@excalidraw/excalidraw";
`FONT_FAMILY` contains all the font families used in `Excalidraw` as explained below
| Font Family | Description |
| ----------- | ---------------------- |
| `Virgil` | The `handwritten` font |
| `Helvetica` | The `Normal` Font |
| `Cascadia` | The `Code` Font |
| Font Family | Description |
| ------------ | ------------------------------------------- |
| `HAND_DRAWN` | The handwritten font (by default, `Virgil`) |
| `NORMAL` | The regular font (by default, `Helvetica`) |
| `CODE` | The code font (by default, `Cascadia`) |
Defaults to `FONT_FAMILY.Virgil` unless passed in `initialData.appState.currentItemFontFamily`.
Defaults to `HAND_DRAWN` unless passed in `initialData.appState.currentItemFontFamily`.
### THEME

View File

@ -10,6 +10,7 @@ import {
computeBoundTextPosition,
computeContainerDimensionForBoundText,
getBoundTextElement,
getFontString,
measureText,
redrawTextBoundingBox,
} from "../element/textElement";
@ -31,7 +32,6 @@ import {
} from "../element/types";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import { getFontString } from "../utils";
import { register } from "./register";
export const actionUnbindText = register({

View File

@ -74,7 +74,7 @@ import {
ExcalidrawElement,
ExcalidrawLinearElement,
ExcalidrawTextElement,
FontFamilyValues,
FontFamilyId,
TextAlign,
VerticalAlign,
} from "../element/types";
@ -689,22 +689,22 @@ export const actionChangeFontFamily = register({
},
PanelComponent: ({ elements, appState, updateData }) => {
const options: {
value: FontFamilyValues;
value: FontFamilyId;
text: string;
icon: JSX.Element;
}[] = [
{
value: FONT_FAMILY.Virgil,
value: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
text: t("labels.handDrawn"),
icon: FreedrawIcon,
},
{
value: FONT_FAMILY.Helvetica,
value: FONT_FAMILY.NORMAL.fontFamilyId,
text: t("labels.normal"),
icon: FontFamilyNormalIcon,
},
{
value: FONT_FAMILY.Cascadia,
value: FONT_FAMILY.CODE.fontFamilyId,
text: t("labels.code"),
icon: FontFamilyCodeIcon,
},
@ -713,7 +713,7 @@ export const actionChangeFontFamily = register({
return (
<fieldset>
<legend>{t("labels.fontFamily")}</legend>
<ButtonIconSelect<FontFamilyValues | false>
<ButtonIconSelect<FontFamilyId | false>
group="font-family"
options={options}
value={getFormValue(

View File

@ -231,7 +231,6 @@ import {
import {
debounce,
distance,
getFontString,
getNearestScrollableContainer,
isInputLike,
isToolIcon,
@ -298,6 +297,7 @@ import {
getContainerCenter,
getContainerElement,
getDefaultLineHeight,
getFontString,
getLineHeightInPx,
getTextBindableContainerAtPosition,
isMeasureTextSupported,

View File

@ -1,6 +1,6 @@
import cssVariables from "./css/variables.module.scss";
import { AppProps } from "./types";
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
import { ExcalidrawElement, FontFamilyId } from "./element/types";
import { COLOR_PALETTE } from "./colors";
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
@ -94,10 +94,19 @@ export const CLASSES = {
// 1-based in case we ever do `if(element.fontFamily)`
export const FONT_FAMILY = {
Virgil: 1,
Helvetica: 2,
Cascadia: 3,
};
HAND_DRAWN: {
fontFamilyId: 1,
fontFamily: "Virgil",
},
NORMAL: {
fontFamilyId: 2,
fontFamily: "Helvetica",
},
CODE: {
fontFamilyId: 3,
fontFamily: "Cascadia",
},
} as const;
export const THEME = {
LIGHT: "light",
@ -119,7 +128,8 @@ export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji";
export const MIN_FONT_SIZE = 1;
export const DEFAULT_FONT_SIZE = 20;
export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
export const DEFAULT_FONT_FAMILY: FontFamilyId =
FONT_FAMILY.HAND_DRAWN.fontFamilyId;
export const DEFAULT_TEXT_ALIGN = "left";
export const DEFAULT_VERTICAL_ALIGN = "top";
export const DEFAULT_VERSION = "{version}";

View File

@ -2,7 +2,6 @@ import {
ExcalidrawElement,
ExcalidrawSelectionElement,
ExcalidrawTextElement,
FontFamilyValues,
PointBinding,
StrokeRoundness,
} from "../element/types";
@ -22,11 +21,9 @@ import {
import { isTextElement, isUsingAdaptiveRadius } from "../element/typeChecks";
import { randomId } from "../random";
import {
DEFAULT_FONT_FAMILY,
DEFAULT_TEXT_ALIGN,
DEFAULT_VERTICAL_ALIGN,
PRECEDING_ELEMENT_KEY,
FONT_FAMILY,
ROUNDNESS,
DEFAULT_SIDEBAR,
DEFAULT_ELEMENT_PROPS,
@ -34,12 +31,14 @@ import {
import { getDefaultAppState } from "../appState";
import { LinearElementEditor } from "../element/linearElementEditor";
import { bumpVersion } from "../element/mutateElement";
import { getFontString, getUpdatedTimestamp, updateActiveTool } from "../utils";
import { getUpdatedTimestamp, updateActiveTool } from "../utils";
import { arrayToMap } from "../utils";
import { MarkOptional, Mutable } from "../utility-types";
import {
detectLineHeight,
getDefaultLineHeight,
getFontFamilyIdByName,
getFontString,
measureBaseline,
} from "../element/textElement";
import { normalizeLink } from "./url";
@ -75,15 +74,6 @@ export type RestoredDataState = {
files: BinaryFiles;
};
const getFontFamilyByName = (fontFamilyName: string): FontFamilyValues => {
if (Object.keys(FONT_FAMILY).includes(fontFamilyName)) {
return FONT_FAMILY[
fontFamilyName as keyof typeof FONT_FAMILY
] as FontFamilyValues;
}
return DEFAULT_FONT_FAMILY;
};
const repairBinding = (binding: PointBinding | null) => {
if (!binding) {
return null;
@ -186,7 +176,7 @@ const restoreElement = (
element as any
).font.split(" ");
fontSize = parseFloat(fontPx);
fontFamily = getFontFamilyByName(_fontFamily);
fontFamily = getFontFamilyIdByName(_fontFamily);
}
const text = element.text ?? "";

View File

@ -17,6 +17,7 @@ import {
} from "../element/newElement";
import {
getDefaultLineHeight,
getFontString,
measureText,
normalizeText,
} from "../element/textElement";
@ -33,12 +34,12 @@ import {
ExcalidrawSelectionElement,
ExcalidrawTextElement,
FileId,
FontFamilyValues,
FontFamilyId,
TextAlign,
VerticalAlign,
} from "../element/types";
import { MarkOptional } from "../utility-types";
import { assertNever, getFontString } from "../utils";
import { assertNever } from "../utils";
export type ValidLinearElement = {
type: "arrow" | "line";
@ -47,7 +48,7 @@ export type ValidLinearElement = {
label?: {
text: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
fontFamily?: FontFamilyId;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
} & MarkOptional<ElementConstructorOpts, "x" | "y">;
@ -124,7 +125,7 @@ export type ValidContainer =
label?: {
text: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
fontFamily?: FontFamilyId;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
} & MarkOptional<ElementConstructorOpts, "x" | "y">;

View File

@ -2,9 +2,9 @@ import { register } from "../actions/register";
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
import { t } from "../i18n";
import { ExcalidrawProps } from "../types";
import { getFontString, setCursorForShape, updateActiveTool } from "../utils";
import { setCursorForShape, updateActiveTool } from "../utils";
import { newTextElement } from "./newElement";
import { getContainerElement, wrapText } from "./textElement";
import { getContainerElement, getFontString, wrapText } from "./textElement";
import { isEmbeddableElement } from "./typeChecks";
import {
ExcalidrawElement,
@ -218,7 +218,7 @@ export const createPlaceholderEmbeddableLabel = (
Math.min(element.width / 2, element.width / text.length),
element.width / 30,
);
const fontFamily = FONT_FAMILY.Helvetica;
const fontFamily = FONT_FAMILY.NORMAL.fontFamilyId;
const fontString = getFontString({
fontSize,

View File

@ -79,7 +79,7 @@ describe("duplicating single elements", () => {
opacity: 100,
text: "hello",
fontSize: 20,
fontFamily: FONT_FAMILY.Virgil,
fontFamily: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
textAlign: "left",
verticalAlign: "top",
});

View File

@ -10,17 +10,12 @@ import {
VerticalAlign,
Arrowhead,
ExcalidrawFreeDrawElement,
FontFamilyValues,
FontFamilyId,
ExcalidrawTextContainer,
ExcalidrawFrameElement,
ExcalidrawEmbeddableElement,
} from "../element/types";
import {
arrayToMap,
getFontString,
getUpdatedTimestamp,
isTestEnv,
} from "../utils";
import { arrayToMap, getUpdatedTimestamp, isTestEnv } from "../utils";
import { randomInteger, randomId } from "../random";
import { bumpVersion, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
@ -35,6 +30,7 @@ import {
wrapText,
getBoundTextMaxWidth,
getDefaultLineHeight,
getFontString,
} from "./textElement";
import {
DEFAULT_ELEMENT_PROPS,
@ -184,7 +180,7 @@ export const newTextElement = (
opts: {
text: string;
fontSize?: number;
fontFamily?: FontFamilyValues;
fontFamily?: FontFamilyId;
textAlign?: TextAlign;
verticalAlign?: VerticalAlign;
containerId?: ExcalidrawTextContainer["id"] | null;

View File

@ -34,7 +34,6 @@ import {
isTextElement,
} from "./typeChecks";
import { mutateElement } from "./mutateElement";
import { getFontString } from "../utils";
import { updateBoundElements } from "./binding";
import {
TransformHandleType,
@ -53,6 +52,7 @@ import {
getApproxMinLineHeight,
measureText,
getBoundTextMaxHeight,
getFontString,
} from "./textElement";
import { LinearElementEditor } from "./linearElementEditor";

View File

@ -427,6 +427,6 @@ describe("Test getDefaultLineHeight", () => {
});
it("should return correct line height", () => {
expect(getDefaultLineHeight(FONT_FAMILY.Cascadia)).toBe(1.2);
expect(getDefaultLineHeight(FONT_FAMILY.CODE.fontFamilyId)).toBe(1.2);
});
});

View File

@ -1,10 +1,10 @@
import { getFontString, arrayToMap, isTestEnv } from "../utils";
import { arrayToMap, isTestEnv } from "../utils";
import {
ExcalidrawElement,
ExcalidrawTextContainer,
ExcalidrawTextElement,
ExcalidrawTextElementWithContainer,
FontFamilyValues,
FontFamilyId,
FontString,
NonDeletedExcalidrawElement,
} from "./types";
@ -19,6 +19,7 @@ import {
isSafari,
TEXT_ALIGN,
VERTICAL_ALIGN,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "../constants";
import { MaybeTransformHandleType } from "./transformHandles";
import Scene from "../scene/Scene";
@ -967,17 +968,57 @@ export const isMeasureTextSupported = () => {
const DEFAULT_LINE_HEIGHT = {
// ~1.25 is the average for Virgil in WebKit and Blink.
// Gecko (FF) uses ~1.28.
[FONT_FAMILY.Virgil]: 1.25 as ExcalidrawTextElement["lineHeight"],
[FONT_FAMILY.HAND_DRAWN.fontFamilyId]:
1.25 as ExcalidrawTextElement["lineHeight"],
// ~1.15 is the average for Virgil in WebKit and Blink.
// Gecko if all over the place.
[FONT_FAMILY.Helvetica]: 1.15 as ExcalidrawTextElement["lineHeight"],
[FONT_FAMILY.NORMAL.fontFamilyId]:
1.15 as ExcalidrawTextElement["lineHeight"],
// ~1.2 is the average for Virgil in WebKit and Blink, and kinda Gecko too
[FONT_FAMILY.Cascadia]: 1.2 as ExcalidrawTextElement["lineHeight"],
[FONT_FAMILY.CODE.fontFamilyId]: 1.2 as ExcalidrawTextElement["lineHeight"],
};
export const getDefaultLineHeight = (fontFamily: FontFamilyValues) => {
if (fontFamily in DEFAULT_LINE_HEIGHT) {
return DEFAULT_LINE_HEIGHT[fontFamily];
export const getDefaultLineHeight = (fontId: number) => {
if (fontId in DEFAULT_LINE_HEIGHT) {
return (
DEFAULT_LINE_HEIGHT as Record<number, ExcalidrawTextElement["lineHeight"]>
)[fontId];
}
return DEFAULT_LINE_HEIGHT[DEFAULT_FONT_FAMILY];
};
export const getFontFamilyIdByName = (fontFamilyName: string): FontFamilyId => {
for (const key in FONT_FAMILY) {
const font = FONT_FAMILY[key as keyof typeof FONT_FAMILY];
if (font.fontFamily === fontFamilyName) {
return font.fontFamilyId;
}
}
return DEFAULT_FONT_FAMILY;
};
export const getFontFamilyString = ({
fontFamily,
}: {
fontFamily: FontFamilyId;
}) => {
for (const key in FONT_FAMILY) {
const font = FONT_FAMILY[key as keyof typeof FONT_FAMILY];
if (font.fontFamilyId === fontFamily) {
return `${font.fontFamily}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
}
}
return WINDOWS_EMOJI_FALLBACK_FONT;
};
/** returns fontSize+fontFamily string for assignment to DOM elements */
export const getFontString = ({
fontSize,
fontFamily,
}: {
fontSize: number;
fontFamily: FontFamilyId;
}) => {
return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;
};

View File

@ -798,7 +798,7 @@ describe("textWysiwyg", () => {
await new Promise((r) => setTimeout(r, 0));
updateTextEditor(editor, "Hello World!");
editor.blur();
expect(text.fontFamily).toEqual(FONT_FAMILY.Virgil);
expect(text.fontFamily).toEqual(FONT_FAMILY.HAND_DRAWN.fontFamilyId);
UI.clickTool("text");
mouse.clickAt(
@ -815,7 +815,7 @@ describe("textWysiwyg", () => {
editor.blur();
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
).toEqual(FONT_FAMILY.CODE.fontFamilyId);
//undo
Keyboard.withModifierKeys({ ctrl: true }, () => {
@ -823,7 +823,7 @@ describe("textWysiwyg", () => {
});
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Virgil);
).toEqual(FONT_FAMILY.HAND_DRAWN.fontFamilyId);
//redo
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
@ -831,7 +831,7 @@ describe("textWysiwyg", () => {
});
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
).toEqual(FONT_FAMILY.CODE.fontFamilyId);
});
it("should wrap text and vertcially center align once text submitted", async () => {
@ -1220,7 +1220,7 @@ describe("textWysiwyg", () => {
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
).toEqual(FONT_FAMILY.CODE.fontFamilyId);
expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75);
fireEvent.click(screen.getByTitle(/Very large/i));
@ -1247,7 +1247,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle(/code/i));
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Cascadia);
).toEqual(FONT_FAMILY.CODE.fontFamilyId);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.2);
@ -1255,7 +1255,7 @@ describe("textWysiwyg", () => {
fireEvent.click(screen.getByTitle(/normal/i));
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).fontFamily,
).toEqual(FONT_FAMILY.Helvetica);
).toEqual(FONT_FAMILY.NORMAL.fontFamilyId);
expect(
(h.elements[1] as ExcalidrawTextElementWithContainer).lineHeight,
).toEqual(1.15);

View File

@ -1,10 +1,5 @@
import { CODES, KEYS } from "../keys";
import {
isWritableElement,
getFontString,
getFontFamilyString,
isTestEnv,
} from "../utils";
import { isWritableElement, isTestEnv } from "../utils";
import Scene from "../scene/Scene";
import {
isArrowElement,
@ -34,6 +29,8 @@ import {
computeContainerDimensionForBoundText,
detectLineHeight,
computeBoundTextPosition,
getFontString,
getFontFamilyString,
} from "./textElement";
import {
actionDecreaseFontSize,

View File

@ -10,8 +10,8 @@ import { MarkNonNullable, ValueOf } from "../utility-types";
export type ChartType = "bar" | "line";
export type FillStyle = "hachure" | "cross-hatch" | "solid" | "zigzag";
export type FontFamilyKeys = keyof typeof FONT_FAMILY;
export type FontFamilyValues = typeof FONT_FAMILY[FontFamilyKeys];
export type FontFamilyId =
typeof FONT_FAMILY[keyof typeof FONT_FAMILY]["fontFamilyId"];
export type Theme = typeof THEME[keyof typeof THEME];
export type FontString = string & { _brand: "fontString" };
export type GroupId = string;
@ -150,7 +150,7 @@ export type ExcalidrawTextElement = _ExcalidrawElementBase &
Readonly<{
type: "text";
fontSize: number;
fontFamily: FontFamilyValues;
fontFamily: FontFamilyId;
text: string;
baseline: number;
textAlign: TextAlign;

View File

@ -1,5 +1,6 @@
import { ExcalidrawElementSkeleton } from "../../../data/transform";
import { FileId } from "../../../element/types";
import { FONT_FAMILY } from "../entry";
const elements: ExcalidrawElementSkeleton[] = [
{
@ -39,7 +40,10 @@ const elements: ExcalidrawElementSkeleton[] = [
];
export default {
elements,
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
appState: {
viewBackgroundColor: "#AFEEEE",
currentItemFontFamily: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
},
scrollToContent: true,
libraryItems: [
[

View File

@ -20,7 +20,7 @@ import type { Drawable } from "roughjs/bin/core";
import type { RoughSVG } from "roughjs/bin/svg";
import { StaticCanvasRenderConfig } from "../scene/types";
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
import { distance, isRTL } from "../utils";
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
import rough from "roughjs/bin/rough";
import {
@ -46,6 +46,8 @@ import {
getLineHeightInPx,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
getFontFamilyString,
getFontString,
} from "../element/textElement";
import { LinearElementEditor } from "../element/linearElementEditor";
import {

View File

@ -1,8 +1,8 @@
import { isTextElement, refreshTextDimensions } from "../element";
import { newElementWith } from "../element/mutateElement";
import { getFontString } from "../element/textElement";
import { isBoundToContainer } from "../element/typeChecks";
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
import { getFontString } from "../utils";
import type Scene from "./Scene";
import { ShapeCache } from "./ShapeCache";

View File

@ -58,7 +58,7 @@ describe("restoreElements", () => {
const textElement = API.createElement({
type: "text",
fontSize: 14,
fontFamily: FONT_FAMILY.Virgil,
fontFamily: FONT_FAMILY.HAND_DRAWN.fontFamilyId,
text: "text",
textAlign: "center",
verticalAlign: "middle",

View File

@ -666,9 +666,13 @@ describe("regression tests", () => {
it("updates fontSize & fontFamily appState", () => {
UI.clickTool("text");
expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Virgil);
expect(h.state.currentItemFontFamily).toEqual(
FONT_FAMILY.HAND_DRAWN.fontFamilyId,
);
fireEvent.click(screen.getByTitle(/code/i));
expect(h.state.currentItemFontFamily).toEqual(FONT_FAMILY.Cascadia);
expect(h.state.currentItemFontFamily).toEqual(
FONT_FAMILY.CODE.fontFamilyId,
);
});
it("deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element", () => {

View File

@ -10,7 +10,7 @@ import {
ExcalidrawBindableElement,
Arrowhead,
ChartType,
FontFamilyValues,
FontFamilyId,
FileId,
ExcalidrawImageElement,
Theme,
@ -221,7 +221,7 @@ export type AppState = {
currentItemStrokeStyle: ExcalidrawElement["strokeStyle"];
currentItemRoughness: number;
currentItemOpacity: number;
currentItemFontFamily: FontFamilyValues;
currentItemFontFamily: FontFamilyId;
currentItemFontSize: number;
currentItemTextAlign: TextAlign;
currentItemStartArrowhead: Arrowhead | null;

View File

@ -4,17 +4,11 @@ import {
CURSOR_TYPE,
DEFAULT_VERSION,
EVENT,
FONT_FAMILY,
isDarwin,
MIME_TYPES,
THEME,
WINDOWS_EMOJI_FALLBACK_FONT,
} from "./constants";
import {
FontFamilyValues,
FontString,
NonDeletedExcalidrawElement,
} from "./element/types";
import { NonDeletedExcalidrawElement } from "./element/types";
import { AppState, DataURL, LastActiveTool, Zoom } from "./types";
import { unstable_batchedUpdates } from "react-dom";
import { SHAPES } from "./shapes";
@ -85,30 +79,6 @@ export const isWritableElement = (
(target instanceof HTMLInputElement &&
(target.type === "text" || target.type === "number"));
export const getFontFamilyString = ({
fontFamily,
}: {
fontFamily: FontFamilyValues;
}) => {
for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) {
if (id === fontFamily) {
return `${fontFamilyString}, ${WINDOWS_EMOJI_FALLBACK_FONT}`;
}
}
return WINDOWS_EMOJI_FALLBACK_FONT;
};
/** returns fontSize+fontFamily string for assignment to DOM elements */
export const getFontString = ({
fontSize,
fontFamily,
}: {
fontSize: number;
fontFamily: FontFamilyValues;
}) => {
return `${fontSize}px ${getFontFamilyString({ fontFamily })}` as FontString;
};
export const debounce = <T extends any[]>(
fn: (...args: T) => void,
timeout: number,