perf: use UIAppState where possible to reduce UI rerenders (#6560)

This commit is contained in:
David Luzar 2023-05-08 10:14:02 +02:00 committed by GitHub
parent 026949204d
commit 560231d365
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 155 additions and 125 deletions

View File

@ -14,7 +14,7 @@ import {
hasText,
} from "../scene";
import { SHAPES } from "../shapes";
import { AppState, Zoom } from "../types";
import { UIAppState, Zoom } from "../types";
import {
capitalizeString,
isTransparent,
@ -28,19 +28,20 @@ import { trackEvent } from "../analytics";
import { hasBoundTextElement } from "../element/typeChecks";
import clsx from "clsx";
import { actionToggleZenMode } from "../actions";
import "./Actions.scss";
import { Tooltip } from "./Tooltip";
import {
shouldAllowVerticalAlign,
suppportsHorizontalAlign,
} from "../element/textElement";
import "./Actions.scss";
export const SelectedShapeActions = ({
appState,
elements,
renderAction,
}: {
appState: AppState;
appState: UIAppState;
elements: readonly ExcalidrawElement[];
renderAction: ActionManager["renderAction"];
}) => {
@ -215,10 +216,10 @@ export const ShapesSwitcher = ({
appState,
}: {
canvas: HTMLCanvasElement | null;
activeTool: AppState["activeTool"];
setAppState: React.Component<any, AppState>["setState"];
activeTool: UIAppState["activeTool"];
setAppState: React.Component<any, UIAppState>["setState"];
onImageAction: (data: { pointerType: PointerType | null }) => void;
appState: AppState;
appState: UIAppState;
}) => (
<>
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {

View File

@ -1,9 +1,7 @@
import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene";
import "./HintViewer.scss";
import { AppState, Device } from "../types";
import { Device, UIAppState } from "../types";
import {
isImageElement,
isLinearElement,
@ -13,8 +11,10 @@ import {
import { getShortcutKey } from "../utils";
import { isEraserActive } from "../appState";
import "./HintViewer.scss";
interface HintViewerProps {
appState: AppState;
appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean;
device: Device;

View File

@ -4,11 +4,10 @@ import { canvasToBlob } from "../data/blob";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState, BinaryFiles } from "../types";
import { BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog";
import { clipboard } from "./icons";
import Stack from "./Stack";
import "./ExportDialog.scss";
import OpenColor from "open-color";
import { CheckboxItem } from "./CheckboxItem";
import { DEFAULT_EXPORT_PADDING, isFirefox } from "../constants";
@ -16,6 +15,8 @@ import { nativeFileSystemSupported } from "../data/filesystem";
import { ActionManager } from "../actions/manager";
import { exportToCanvas } from "../packages/utils";
import "./ExportDialog.scss";
const supportsContextFilters =
"filter" in document.createElement("canvas").getContext("2d")!;
@ -70,7 +71,7 @@ const ImageExportModal = ({
onExportToSvg,
onExportToClipboard,
}: {
appState: AppState;
appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
exportPadding?: number;
@ -216,8 +217,8 @@ export const ImageExportDialog = ({
onExportToSvg,
onExportToClipboard,
}: {
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
appState: UIAppState;
setAppState: React.Component<any, UIAppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
files: BinaryFiles;
exportPadding?: number;

View File

@ -2,7 +2,7 @@ import React from "react";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { AppState, ExportOpts, BinaryFiles } from "../types";
import { ExportOpts, BinaryFiles, UIAppState } from "../types";
import { Dialog } from "./Dialog";
import { exportToFileIcon, LinkIcon } from "./icons";
import { ToolButton } from "./ToolButton";
@ -28,7 +28,7 @@ const JSONExportModal = ({
exportOpts,
canvas,
}: {
appState: AppState;
appState: UIAppState;
files: BinaryFiles;
elements: readonly NonDeletedExcalidrawElement[];
actionManager: ActionManager;
@ -96,12 +96,12 @@ export const JSONExportDialog = ({
setAppState,
}: {
elements: readonly NonDeletedExcalidrawElement[];
appState: AppState;
appState: UIAppState;
files: BinaryFiles;
actionManager: ActionManager;
exportOpts: ExportOpts;
canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"];
setAppState: React.Component<any, UIAppState>["setState"];
}) => {
const handleClose = React.useCallback(() => {
setAppState({ openDialog: null });

View File

@ -8,7 +8,13 @@ import { NonDeletedExcalidrawElement } from "../element/types";
import { Language, t } from "../i18n";
import { calculateScrollCenter } from "../scene";
import { ExportType } from "../scene/types";
import { AppProps, AppState, ExcalidrawProps, BinaryFiles } from "../types";
import {
AppProps,
AppState,
ExcalidrawProps,
BinaryFiles,
UIAppState,
} from "../types";
import { capitalizeString, isShallowEqual, muteFSAbortError } from "../utils";
import { SelectedShapeActions, ShapesSwitcher } from "./Actions";
import { ErrorDialog } from "./ErrorDialog";
@ -49,7 +55,7 @@ import "./Toolbar.scss";
interface LayerUIProps {
actionManager: ActionManager;
appState: AppState;
appState: UIAppState;
files: BinaryFiles;
canvas: HTMLCanvasElement | null;
setAppState: React.Component<any, AppState>["setState"];
@ -144,7 +150,8 @@ const LayerUI = ({
const fileHandle = await exportCanvas(
type,
exportedElements,
appState,
// FIXME once we split UI canvas from element canvas
appState as AppState,
files,
{
exportBackground: appState.exportBackground,
@ -458,9 +465,9 @@ const LayerUI = ({
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
setAppState((appState) => ({
...calculateScrollCenter(elements, appState, canvas),
});
}));
}}
>
{t("buttons.scrollBackToContent")}
@ -484,14 +491,15 @@ const LayerUI = ({
);
};
const stripIrrelevantAppStateProps = (
appState: AppState,
): Omit<
AppState,
"suggestedBindings" | "startBoundElement" | "cursorButton"
> => {
const { suggestedBindings, startBoundElement, cursorButton, ...ret } =
appState;
const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
const {
suggestedBindings,
startBoundElement,
cursorButton,
scrollX,
scrollY,
...ret
} = appState;
return ret;
};
@ -506,8 +514,10 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
return (
isShallowEqual(
stripIrrelevantAppStateProps(prevAppState),
stripIrrelevantAppStateProps(nextAppState),
// asserting AppState because we're being passed the whole AppState
// but resolve to only the UI-relevant props
stripIrrelevantAppStateProps(prevAppState as AppState),
stripIrrelevantAppStateProps(nextAppState as AppState),
{
selectedElementIds: isShallowEqual,
selectedGroupIds: isShallowEqual,

View File

@ -5,7 +5,12 @@ import Library, {
} from "../data/library";
import { t } from "../i18n";
import { randomId } from "../random";
import { LibraryItems, LibraryItem, AppState, ExcalidrawProps } from "../types";
import {
LibraryItems,
LibraryItem,
ExcalidrawProps,
UIAppState,
} from "../types";
import LibraryMenuItems from "./LibraryMenuItems";
import { trackEvent } from "../analytics";
import { atom, useAtom } from "jotai";
@ -44,11 +49,11 @@ export const LibraryMenuContent = ({
pendingElements: LibraryItem["elements"];
onInsertLibraryItems: (libraryItems: LibraryItems) => void;
onAddToLibrary: () => void;
setAppState: React.Component<any, AppState>["setState"];
setAppState: React.Component<any, UIAppState>["setState"];
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
library: Library;
id: string;
appState: AppState;
appState: UIAppState;
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => {

View File

@ -1,6 +1,6 @@
import { VERSIONS } from "../constants";
import { t } from "../i18n";
import { AppState, ExcalidrawProps } from "../types";
import { ExcalidrawProps, UIAppState } from "../types";
const LibraryMenuBrowseButton = ({
theme,
@ -8,7 +8,7 @@ const LibraryMenuBrowseButton = ({
libraryReturnUrl,
}: {
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: AppState["theme"];
theme: UIAppState["theme"];
id: string;
}) => {
const referrer =

View File

@ -1,4 +1,4 @@
import { LibraryItem, ExcalidrawProps, AppState } from "../types";
import { LibraryItem, ExcalidrawProps, UIAppState } from "../types";
import LibraryMenuBrowseButton from "./LibraryMenuBrowseButton";
import { LibraryDropdownMenu } from "./LibraryMenuHeaderContent";
@ -13,7 +13,7 @@ export const LibraryMenuControlButtons = ({
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: AppState["theme"];
theme: UIAppState["theme"];
id: string;
style: React.CSSProperties;
}) => {

View File

@ -1,8 +1,8 @@
import { useCallback, useState } from "react";
import { t } from "../i18n";
import { jotaiScope } from "../jotai";
import { AppState, LibraryItem, LibraryItems } from "../types";
import { useApp, useExcalidrawAppState, useExcalidrawSetAppState } from "./App";
import { LibraryItem, LibraryItems, UIAppState } from "../types";
import { useApp, useExcalidrawSetAppState } from "./App";
import { saveLibraryAsJSON } from "../data/json";
import Library, { libraryItemsAtom } from "../data/library";
import {
@ -21,6 +21,7 @@ import PublishLibrary from "./PublishLibrary";
import { Dialog } from "./Dialog";
import DropdownMenu from "./dropdownMenu/DropdownMenu";
import { isLibraryMenuOpenAtom } from "./LibraryMenu";
import { useUIAppState } from "../context/ui-appState";
const getSelectedItems = (
libraryItems: LibraryItems,
@ -28,13 +29,13 @@ const getSelectedItems = (
) => libraryItems.filter((item) => selectedItems.includes(item.id));
export const LibraryDropdownMenuButton: React.FC<{
setAppState: React.Component<any, AppState>["setState"];
setAppState: React.Component<any, UIAppState>["setState"];
selectedItems: LibraryItem["id"][];
library: Library;
onRemoveFromLibrary: () => void;
resetLibrary: () => void;
onSelectItems: (items: LibraryItem["id"][]) => void;
appState: AppState;
appState: UIAppState;
}> = ({
setAppState,
selectedItems,
@ -270,7 +271,7 @@ export const LibraryDropdownMenu = ({
onSelectItems: (id: LibraryItem["id"][]) => void;
}) => {
const { library } = useApp();
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const [libraryItemsData] = useAtom(libraryItemsAtom, jotaiScope);

View File

@ -2,17 +2,22 @@ import React, { useState } from "react";
import { serializeLibraryAsJSON } from "../data/json";
import { ExcalidrawElement, NonDeleted } from "../element/types";
import { t } from "../i18n";
import { AppState, ExcalidrawProps, LibraryItem, LibraryItems } from "../types";
import {
ExcalidrawProps,
LibraryItem,
LibraryItems,
UIAppState,
} from "../types";
import { arrayToMap, chunk } from "../utils";
import { LibraryUnit } from "./LibraryUnit";
import Stack from "./Stack";
import "./LibraryMenuItems.scss";
import { MIME_TYPES } from "../constants";
import Spinner from "./Spinner";
import { duplicateElements } from "../element/newElement";
import { LibraryMenuControlButtons } from "./LibraryMenuControlButtons";
import "./LibraryMenuItems.scss";
const CELLS_PER_ROW = 4;
const LibraryMenuItems = ({
@ -35,7 +40,7 @@ const LibraryMenuItems = ({
selectedItems: LibraryItem["id"][];
onSelectItems: (id: LibraryItem["id"][]) => void;
libraryReturnUrl: ExcalidrawProps["libraryReturnUrl"];
theme: AppState["theme"];
theme: UIAppState["theme"];
id: string;
}) => {
const [lastSelectedItem, setLastSelectedItem] = useState<

View File

@ -1,5 +1,5 @@
import React from "react";
import { AppState, Device, ExcalidrawProps } from "../types";
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@ -21,7 +21,7 @@ import { isHandToolActive } from "../appState";
import { useTunnels } from "../context/tunnels";
type MobileMenuProps = {
appState: AppState;
appState: UIAppState;
actionManager: ActionManager;
renderJSONExportDialog: () => React.ReactNode;
renderImageExportDialog: () => React.ReactNode;
@ -35,7 +35,7 @@ type MobileMenuProps = {
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
renderTopRightUI?: (
isMobile: boolean,
appState: AppState,
appState: UIAppState,
) => JSX.Element | null;
renderCustomStats?: ExcalidrawProps["renderCustomStats"];
renderSidebars: () => JSX.Element | null;
@ -193,9 +193,9 @@ export const MobileMenu = ({
<button
className="scroll-back-to-content"
onClick={() => {
setAppState({
setAppState((appState) => ({
...calculateScrollCenter(elements, appState, canvas),
});
}));
}}
>
{t("buttons.scrollBackToContent")}

View File

@ -5,9 +5,10 @@ import { ChartElements, renderSpreadsheet, Spreadsheet } from "../charts";
import { ChartType } from "../element/types";
import { t } from "../i18n";
import { exportToSvg } from "../scene/export";
import { AppState } from "../types";
import { UIAppState } from "../types";
import { useApp } from "./App";
import { Dialog } from "./Dialog";
import "./PasteChartDialog.scss";
type OnInsertChart = (chartType: ChartType, elements: ChartElements) => void;
@ -80,9 +81,9 @@ export const PasteChartDialog = ({
appState,
onClose,
}: {
appState: AppState;
appState: UIAppState;
onClose: () => void;
setAppState: React.Component<any, AppState>["setState"];
setAppState: React.Component<any, UIAppState>["setState"];
}) => {
const { onInsertElements } = useApp();
const handleClose = React.useCallback(() => {

View File

@ -4,7 +4,7 @@ import OpenColor from "open-color";
import { Dialog } from "./Dialog";
import { t } from "../i18n";
import { AppState, LibraryItems, LibraryItem } from "../types";
import { LibraryItems, LibraryItem, UIAppState } from "../types";
import { exportToCanvas, exportToSvg } from "../packages/utils";
import {
EXPORT_DATA_TYPES,
@ -135,7 +135,7 @@ const SingleLibraryItem = ({
onRemove,
}: {
libItem: LibraryItem;
appState: AppState;
appState: UIAppState;
index: number;
onChange: (val: string, index: number) => void;
onRemove: (id: string) => void;
@ -231,7 +231,7 @@ const PublishLibrary = ({
}: {
onClose: () => void;
libraryItems: LibraryItems;
appState: AppState;
appState: UIAppState;
onSuccess: (data: {
url: string;
authorName: string;

View File

@ -18,11 +18,7 @@ import {
} from "./common";
import { SidebarHeader } from "./SidebarHeader";
import clsx from "clsx";
import {
useDevice,
useExcalidrawAppState,
useExcalidrawSetAppState,
} from "../App";
import { useDevice, useExcalidrawSetAppState } from "../App";
import { updateObject } from "../../utils";
import { KEYS } from "../../keys";
import { EVENT } from "../../constants";
@ -33,6 +29,7 @@ import { SidebarTabs } from "./SidebarTabs";
import { SidebarTab } from "./SidebarTab";
import "./Sidebar.scss";
import { useUIAppState } from "../../context/ui-appState";
// FIXME replace this with the implem from ColorPicker once it's merged
const useOnClickOutside = (
@ -185,7 +182,7 @@ SidebarInner.displayName = "SidebarInner";
export const Sidebar = Object.assign(
forwardRef((props: SidebarProps, ref: React.ForwardedRef<HTMLDivElement>) => {
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const { onStateChange } = props;

View File

@ -1,8 +1,9 @@
import { useExcalidrawSetAppState, useExcalidrawAppState } from "../App";
import { useExcalidrawSetAppState } from "../App";
import { SidebarTriggerProps } from "./common";
import { useUIAppState } from "../../context/ui-appState";
import clsx from "clsx";
import "./SidebarTrigger.scss";
import clsx from "clsx";
export const SidebarTrigger = ({
name,
@ -15,8 +16,7 @@ export const SidebarTrigger = ({
style,
}: SidebarTriggerProps) => {
const setAppState = useExcalidrawSetAppState();
// TODO replace with sidebar context
const appState = useExcalidrawAppState();
const appState = useUIAppState();
return (
<label title={title}>

View File

@ -3,14 +3,14 @@ import { getCommonBounds } from "../element/bounds";
import { NonDeletedExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { getTargetElements } from "../scene";
import { AppState, ExcalidrawProps } from "../types";
import { ExcalidrawProps, UIAppState } from "../types";
import { CloseIcon } from "./icons";
import { Island } from "./Island";
import "./Stats.scss";
export const Stats = (props: {
appState: AppState;
setAppState: React.Component<any, AppState>["setState"];
appState: UIAppState;
setAppState: React.Component<any, UIAppState>["setState"];
elements: readonly NonDeletedExcalidrawElement[];
onClose: () => void;
renderCustomStats: ExcalidrawProps["renderCustomStats"];

View File

@ -1,5 +1,6 @@
import clsx from "clsx";
import { useDevice, useExcalidrawAppState } from "../App";
import { useUIAppState } from "../../context/ui-appState";
import { useDevice } from "../App";
const MenuTrigger = ({
className = "",
@ -10,7 +11,7 @@ const MenuTrigger = ({
children: React.ReactNode;
onToggle: () => void;
}) => {
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const device = useDevice();
const classNames = clsx(
`dropdown-menu-button ${className}`,

View File

@ -1,7 +1,6 @@
import clsx from "clsx";
import { actionShortcuts } from "../../actions";
import { ActionManager } from "../../actions/manager";
import { AppState } from "../../types";
import {
ExitZenModeAction,
FinalizeAction,
@ -13,6 +12,7 @@ import { useTunnels } from "../../context/tunnels";
import { HelpButton } from "../HelpButton";
import { Section } from "../Section";
import Stack from "../Stack";
import { UIAppState } from "../../types";
const Footer = ({
appState,
@ -20,7 +20,7 @@ const Footer = ({
showExitZenModeBtn,
renderWelcomeScreen,
}: {
appState: AppState;
appState: UIAppState;
actionManager: ActionManager;
showExitZenModeBtn: boolean;
renderWelcomeScreen: boolean;

View File

@ -1,11 +1,11 @@
import clsx from "clsx";
import { useExcalidrawAppState } from "../App";
import { useTunnels } from "../../context/tunnels";
import "./FooterCenter.scss";
import { useUIAppState } from "../../context/ui-appState";
const FooterCenter = ({ children }: { children?: React.ReactNode }) => {
const { FooterCenterTunnel } = useTunnels();
const appState = useExcalidrawAppState();
const appState = useUIAppState();
return (
<FooterCenterTunnel.In>
<div

View File

@ -3,9 +3,9 @@ import { usersIcon } from "../icons";
import { Button } from "../Button";
import clsx from "clsx";
import { useExcalidrawAppState } from "../App";
import "./LiveCollaborationTrigger.scss";
import { useUIAppState } from "../../context/ui-appState";
const LiveCollaborationTrigger = ({
isCollaborating,
@ -15,7 +15,7 @@ const LiveCollaborationTrigger = ({
isCollaborating: boolean;
onSelect: () => void;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
const appState = useExcalidrawAppState();
const appState = useUIAppState();
return (
<Button

View File

@ -1,10 +1,6 @@
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { useI18n } from "../../i18n";
import {
useExcalidrawAppState,
useExcalidrawSetAppState,
useExcalidrawActionManager,
} from "../App";
import { useExcalidrawSetAppState, useExcalidrawActionManager } from "../App";
import {
ExportIcon,
ExportImageIcon,
@ -32,6 +28,7 @@ import clsx from "clsx";
import { useSetAtom } from "jotai";
import { activeConfirmDialogAtom } from "../ActiveConfirmDialog";
import { jotaiScope } from "../../jotai";
import { useUIAppState } from "../../context/ui-appState";
export const LoadScene = () => {
const { t } = useI18n();
@ -139,7 +136,7 @@ ClearCanvas.displayName = "ClearCanvas";
export const ToggleTheme = () => {
const { t } = useI18n();
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionToggleTheme)) {
@ -172,7 +169,7 @@ ToggleTheme.displayName = "ToggleTheme";
export const ChangeCanvasBackground = () => {
const { t } = useI18n();
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) {

View File

@ -1,9 +1,5 @@
import React from "react";
import {
useDevice,
useExcalidrawAppState,
useExcalidrawSetAppState,
} from "../App";
import { useDevice, useExcalidrawSetAppState } from "../App";
import DropdownMenu from "../dropdownMenu/DropdownMenu";
import * as DefaultItems from "./DefaultItems";
@ -14,6 +10,7 @@ import { HamburgerMenuIcon } from "../icons";
import { withInternalFallback } from "../hoc/withInternalFallback";
import { composeEventHandlers } from "../../utils";
import { useTunnels } from "../../context/tunnels";
import { useUIAppState } from "../../context/ui-appState";
const MainMenu = Object.assign(
withInternalFallback(
@ -30,7 +27,7 @@ const MainMenu = Object.assign(
}) => {
const { MainMenuTunnel } = useTunnels();
const device = useDevice();
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const onClickOutside = device.isMobile
? undefined

View File

@ -1,13 +1,10 @@
import { actionLoadScene, actionShortcuts } from "../../actions";
import { getShortcutFromShortcutName } from "../../actions/shortcuts";
import { t, useI18n } from "../../i18n";
import {
useDevice,
useExcalidrawActionManager,
useExcalidrawAppState,
} from "../App";
import { useDevice, useExcalidrawActionManager } from "../App";
import { useTunnels } from "../../context/tunnels";
import { ExcalLogo, HelpIcon, LoadIcon, usersIcon } from "../icons";
import { useUIAppState } from "../../context/ui-appState";
const WelcomeScreenMenuItemContent = ({
icon,
@ -148,7 +145,7 @@ const MenuItemHelp = () => {
MenuItemHelp.displayName = "MenuItemHelp";
const MenuItemLoadScene = () => {
const appState = useExcalidrawAppState();
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) {

View File

@ -1,5 +1,5 @@
import React from "react";
import { AppState } from "../types";
import { UIAppState } from "../types";
export const UIAppStateContext = React.createContext<AppState>(null!);
export const UIAppStateContext = React.createContext<UIAppState>(null!);
export const useUIAppState = () => React.useContext(UIAppStateContext);

View File

@ -1,4 +1,4 @@
import { AppState, ExcalidrawProps, Point } from "../types";
import { AppState, ExcalidrawProps, Point, UIAppState } from "../types";
import {
getShortcutKey,
sceneCoordsToViewportCoords,
@ -297,10 +297,11 @@ export const getContextMenuLabel = (
: "labels.link.create";
return label;
};
export const getLinkHandleFromCoords = (
[x1, y1, x2, y2]: Bounds,
angle: number,
appState: AppState,
appState: UIAppState,
): [x: number, y: number, width: number, height: number] => {
const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value;

View File

@ -1,9 +1,9 @@
import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "./types";
import { getSelectedElements } from "../scene";
import { UIAppState } from "../types";
export const showSelectedShapeActions = (
appState: AppState,
appState: UIAppState,
elements: readonly NonDeletedExcalidrawElement[],
) =>
Boolean(

View File

@ -7,8 +7,9 @@ import {
import { DEFAULT_VERSION } from "../constants";
import { t } from "../i18n";
import { copyTextToSystemClipboard } from "../clipboard";
import { AppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { UIAppState } from "../types";
type StorageSizes = { scene: number; total: number };
const STORAGE_SIZE_TIMEOUT = 500;
@ -23,7 +24,7 @@ const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
type Props = {
setToast: (message: string) => void;
elements: readonly NonDeletedExcalidrawElement[];
appState: AppState;
appState: UIAppState;
};
const CustomStats = (props: Props) => {
const [storageSizes, setStorageSizes] = useState<StorageSizes>({

View File

@ -18,7 +18,7 @@ import { getFrame } from "../../utils";
const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: Partial<AppState>,
files: BinaryFiles,
) => {
const firebase = await loadFirebaseStorage();
@ -75,7 +75,7 @@ const exportToExcalidrawPlus = async (
export const ExportToExcalidrawPlus: React.FC<{
elements: readonly NonDeletedExcalidrawElement[];
appState: AppState;
appState: Partial<AppState>;
files: BinaryFiles;
onError: (error: Error) => void;
}> = ({ elements, appState, files, onError }) => {

View File

@ -284,7 +284,7 @@ export const loadScene = async (
export const exportToBackend = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
appState: Partial<AppState>,
files: BinaryFiles,
) => {
const encryptionKey = await generateEncryptionKey("string");

View File

@ -32,6 +32,7 @@ import {
ExcalidrawImperativeAPI,
BinaryFiles,
ExcalidrawInitialDataState,
UIAppState,
} from "../types";
import {
debounce,
@ -550,7 +551,7 @@ const ExcalidrawWrapper = () => {
const onExportToBackend = async (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: Partial<AppState>,
files: BinaryFiles,
canvas: HTMLCanvasElement | null,
) => {
@ -581,7 +582,7 @@ const ExcalidrawWrapper = () => {
const renderCustomStats = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: UIAppState,
) => {
return (
<CustomStats

View File

@ -30,7 +30,7 @@ export const getElementsWithinSelection = (
export const isSomeElementSelected = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: Pick<AppState, "selectedElementIds">,
): boolean =>
elements.some((element) => appState.selectedElementIds[element.id]);
@ -40,7 +40,7 @@ export const isSomeElementSelected = (
*/
export const getCommonAttributeOfSelectedElements = <T>(
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: Pick<AppState, "selectedElementIds">,
getAttribute: (element: ExcalidrawElement) => T,
): T | null => {
const attributes = Array.from(
@ -55,7 +55,7 @@ export const getCommonAttributeOfSelectedElements = <T>(
export const getSelectedElements = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: Pick<AppState, "selectedElementIds">,
includeBoundTextElement: boolean = false,
) =>
elements.filter((element) => {
@ -74,7 +74,7 @@ export const getSelectedElements = (
export const getTargetElements = (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
) =>
appState.editingElement
? [appState.editingElement]

View File

@ -1,3 +1,4 @@
import React from "react";
import {
PointerType,
ExcalidrawLinearElement,
@ -32,7 +33,6 @@ import type { FileSystemHandle } from "./data/filesystem";
import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import { ContextMenuItems } from "./components/ContextMenu";
import { Merge, ForwardRef, ValueOf } from "./utility-types";
import React from "react";
export type Point = Readonly<RoughPoint>;
@ -218,6 +218,15 @@ export type AppState = {
selectedLinearElement: LinearElementEditor | null;
};
export type UIAppState = Omit<
AppState,
| "suggestedBindings"
| "startBoundElement"
| "cursorButton"
| "scrollX"
| "scrollY"
>;
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
export type Zoom = Readonly<{
@ -314,7 +323,7 @@ export interface ExcalidrawProps {
) => Promise<boolean> | boolean;
renderTopRightUI?: (
isMobile: boolean,
appState: AppState,
appState: UIAppState,
) => JSX.Element | null;
langCode?: Language["code"];
viewModeEnabled?: boolean;
@ -325,7 +334,7 @@ export interface ExcalidrawProps {
name?: string;
renderCustomStats?: (
elements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: UIAppState,
) => JSX.Element;
UIOptions?: Partial<UIOptions>;
detectScroll?: boolean;
@ -364,13 +373,13 @@ export type ExportOpts = {
saveFileToDisk?: boolean;
onExportToBackend?: (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: UIAppState,
files: BinaryFiles,
canvas: HTMLCanvasElement | null,
) => void;
renderCustomUI?: (
exportedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState,
appState: UIAppState,
files: BinaryFiles,
canvas: HTMLCanvasElement | null,
) => JSX.Element;

View File

@ -372,7 +372,7 @@ export const setEraserCursor = (
export const setCursorForShape = (
canvas: HTMLCanvasElement | null,
appState: AppState,
appState: Pick<AppState, "activeTool" | "theme">,
) => {
if (!canvas) {
return;
@ -787,7 +787,12 @@ export const isShallowEqual = <
? comparator(objA[key], objB[key])
: objA[key] === objB[key];
if (!ret && debug) {
console.warn(`isShallowEqual: ${key} not equal ->`, objA[key], objB[key]);
console.info(
`%cisShallowEqual: ${key} not equal ->`,
"color: #8B4000",
objA[key],
objB[key],
);
}
return ret;
});