perf: use UIAppState
where possible to reduce UI rerenders (#6560)
This commit is contained in:
parent
026949204d
commit
560231d365
@ -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) => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}) => {
|
||||
|
@ -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 =
|
||||
|
@ -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;
|
||||
}) => {
|
||||
|
@ -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);
|
||||
|
@ -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<
|
||||
|
@ -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")}
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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}>
|
||||
|
@ -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"];
|
||||
|
@ -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}`,
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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(
|
||||
|
@ -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>({
|
||||
|
@ -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 }) => {
|
||||
|
@ -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");
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
19
src/types.ts
19
src/types.ts
@ -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;
|
||||
|
@ -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;
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user