import { useEffect, useMemo, useState, memo } from "react"; import { getCommonBounds } from "../../element/bounds"; import type { NonDeletedExcalidrawElement } from "../../element/types"; import { t } from "../../i18n"; import type { AppClassProperties, AppState, ExcalidrawProps, } from "../../types"; import { CloseIcon } from "../icons"; import { Island } from "../Island"; import { throttle } from "lodash"; import Dimension from "./Dimension"; import Angle from "./Angle"; import FontSize from "./FontSize"; import MultiDimension from "./MultiDimension"; import { elementsAreInSameGroup } from "../../groups"; import MultiAngle from "./MultiAngle"; import MultiFontSize from "./MultiFontSize"; import Position from "./Position"; import MultiPosition from "./MultiPosition"; import Collapsible from "./Collapsible"; import { useExcalidrawAppState, useExcalidrawSetAppState } from "../App"; import { getAtomicUnits } from "./utils"; import { STATS_PANELS } from "../../constants"; import { isElbowArrow } from "../../element/typeChecks"; import CanvasGrid from "./CanvasGrid"; import clsx from "clsx"; import "./Stats.scss"; import { isGridModeEnabled } from "../../snapping"; interface StatsProps { app: AppClassProperties; onClose: () => void; renderCustomStats: ExcalidrawProps["renderCustomStats"]; } const STATS_TIMEOUT = 50; export const Stats = (props: StatsProps) => { const appState = useExcalidrawAppState(); const sceneNonce = props.app.scene.getSceneNonce() || 1; const selectedElements = props.app.scene.getSelectedElements({ selectedElementIds: appState.selectedElementIds, includeBoundTextElement: false, }); const gridModeEnabled = isGridModeEnabled(props.app); return ( ); }; const StatsRow = ({ children, columns = 1, heading, style, ...rest }: { children: React.ReactNode; columns?: number; heading?: boolean; style?: React.CSSProperties; } & React.HTMLAttributes) => ( {children} ); StatsRow.displayName = "StatsRow"; const StatsRows = ({ children, order, style, ...rest }: { children: React.ReactNode; order?: number; style?: React.CSSProperties; } & React.HTMLAttributes) => ( {children} ); StatsRows.displayName = "StatsRows"; Stats.StatsRow = StatsRow; Stats.StatsRows = StatsRows; export const StatsInner = memo( ({ app, onClose, renderCustomStats, selectedElements, appState, sceneNonce, gridModeEnabled, }: StatsProps & { sceneNonce: number; selectedElements: readonly NonDeletedExcalidrawElement[]; appState: AppState; gridModeEnabled: boolean; }) => { const scene = app.scene; const elements = scene.getNonDeletedElements(); const elementsMap = scene.getNonDeletedElementsMap(); const setAppState = useExcalidrawSetAppState(); const singleElement = selectedElements.length === 1 ? selectedElements[0] : null; const multipleElements = selectedElements.length > 1 ? selectedElements : null; const [sceneDimension, setSceneDimension] = useState<{ width: number; height: number; }>({ width: 0, height: 0, }); const throttledSetSceneDimension = useMemo( () => throttle((elements: readonly NonDeletedExcalidrawElement[]) => { const boundingBox = getCommonBounds(elements); setSceneDimension({ width: Math.round(boundingBox[2]) - Math.round(boundingBox[0]), height: Math.round(boundingBox[3]) - Math.round(boundingBox[1]), }); }, STATS_TIMEOUT), [], ); useEffect(() => { throttledSetSceneDimension(elements); }, [sceneNonce, elements, throttledSetSceneDimension]); useEffect( () => () => throttledSetSceneDimension.cancel(), [throttledSetSceneDimension], ); const atomicUnits = useMemo(() => { return getAtomicUnits(selectedElements, appState); }, [selectedElements, appState]); return ( {t("stats.title")} {CloseIcon} {t("stats.generalStats")}} open={!!(appState.stats.panels & STATS_PANELS.generalStats)} openTrigger={() => setAppState((state) => { return { stats: { open: true, panels: state.stats.panels ^ STATS_PANELS.generalStats, }, }; }) } > {t("stats.scene")} {t("stats.shapes")} {elements.length} {t("stats.width")} {sceneDimension.width} {t("stats.height")} {sceneDimension.height} {gridModeEnabled && ( <> Canvas > )} {renderCustomStats?.(elements, appState)} {selectedElements.length > 0 && ( {t("stats.elementProperties")}} open={ !!(appState.stats.panels & STATS_PANELS.elementProperties) } openTrigger={() => setAppState((state) => { return { stats: { open: true, panels: state.stats.panels ^ STATS_PANELS.elementProperties, }, }; }) } > {singleElement && ( <> {t(`element.${singleElement.type}`)} {!isElbowArrow(singleElement) && ( )} > )} {multipleElements && ( <> {elementsAreInSameGroup(multipleElements) && ( {t("element.group")} )} {t("stats.shapes")} {selectedElements.length} > )} )} ); }, (prev, next) => { return ( prev.sceneNonce === next.sceneNonce && prev.selectedElements === next.selectedElements && prev.appState.stats.panels === next.appState.stats.panels && prev.gridModeEnabled === next.gridModeEnabled && prev.appState.gridStep === next.appState.gridStep ); }, );