react.memo (#79)

This commit is contained in:
Mike Sawka 2024-06-26 09:31:43 -07:00 committed by GitHub
parent f036459dd5
commit 4f627a0342
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 322 additions and 262 deletions

View File

@ -174,7 +174,7 @@ interface FramelessBlockHeaderProps {
dragHandleRef?: React.RefObject<HTMLDivElement>; dragHandleRef?: React.RefObject<HTMLDivElement>;
} }
const FramelessBlockHeader = ({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => { const FramelessBlockHeader = React.memo(({ blockId, onClose, dragHandleRef }: FramelessBlockHeaderProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId)); const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
@ -193,7 +193,7 @@ const FramelessBlockHeader = ({ blockId, onClose, dragHandleRef }: FramelessBloc
)} )}
</div> </div>
); );
}; });
const hoverStateOff = "off"; const hoverStateOff = "off";
const hoverStatePending = "pending"; const hoverStatePending = "pending";
@ -209,15 +209,8 @@ interface BlockFrameProps {
dragHandleRef?: React.RefObject<HTMLDivElement>; dragHandleRef?: React.RefObject<HTMLDivElement>;
} }
const BlockFrame_Tech = ({ const BlockFrame_Tech = React.memo(
blockId, ({ blockId, onClose, onClick, preview, blockRef, dragHandleRef, children }: BlockFrameProps) => {
onClose,
onClick,
preview,
blockRef,
dragHandleRef,
children,
}: BlockFrameProps) => {
const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId)); const [blockData] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => { const isFocusedAtom = useBlockAtom<boolean>(blockId, "isFocused", () => {
@ -264,7 +257,8 @@ const BlockFrame_Tech = ({
{preview ? <div className="block-frame-preview" /> : children} {preview ? <div className="block-frame-preview" /> : children}
</div> </div>
); );
}; }
);
const BlockFrame_Frameless = ({ const BlockFrame_Frameless = ({
blockId, blockId,
@ -340,7 +334,7 @@ const BlockFrame_Frameless = ({
); );
}; };
const BlockFrame = (props: BlockFrameProps) => { const BlockFrame = React.memo((props: BlockFrameProps) => {
const blockId = props.blockId; const blockId = props.blockId;
const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId)); const [blockData, blockDataLoading] = WOS.useWaveObjectValue<Block>(WOS.makeORef("block", blockId));
const tabData = jotai.useAtomValue(atoms.tabAtom); const tabData = jotai.useAtomValue(atoms.tabAtom);
@ -360,7 +354,7 @@ const BlockFrame = (props: BlockFrameProps) => {
FrameElem = BlockFrame_Frameless; FrameElem = BlockFrame_Frameless;
} }
return <FrameElem {...props} />; return <FrameElem {...props} />;
}; });
function blockViewToIcon(view: string): string { function blockViewToIcon(view: string): string {
console.log("blockViewToIcon", view); console.log("blockViewToIcon", view);
@ -398,7 +392,16 @@ function useBlockIcon(blockId: string): string {
return blockIcon; return blockIcon;
} }
const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => { const wm = new WeakMap();
let wmCounter = 0;
function getObjectId(obj: any): number {
if (!wm.has(obj)) {
wm.set(obj, wmCounter++);
}
return wm.get(obj);
}
const Block = React.memo(({ blockId, onClose, dragHandleRef }: BlockProps) => {
let blockElem: JSX.Element = null; let blockElem: JSX.Element = null;
const focusElemRef = React.useRef<HTMLInputElement>(null); const focusElemRef = React.useRef<HTMLInputElement>(null);
const blockRef = React.useRef<HTMLDivElement>(null); const blockRef = React.useRef<HTMLDivElement>(null);
@ -417,24 +420,29 @@ const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => {
setBlockFocus(blockId); setBlockFocus(blockId);
}, [blockClicked]); }, [blockClicked]);
const setBlockClickedTrue = React.useCallback(() => {
setBlockClicked(true);
}, []);
if (!blockId || !blockData) return null; if (!blockId || !blockData) return null;
if (blockDataLoading) { if (blockDataLoading) {
blockElem = <CenteredDiv>Loading...</CenteredDiv>; blockElem = <CenteredDiv>Loading...</CenteredDiv>;
} else if (blockData.view === "term") { } else if (blockData.view === "term") {
blockElem = <TerminalView blockId={blockId} />; blockElem = <TerminalView key={blockId} blockId={blockId} />;
} else if (blockData.view === "preview") { } else if (blockData.view === "preview") {
blockElem = <PreviewView blockId={blockId} />; blockElem = <PreviewView key={blockId} blockId={blockId} />;
} else if (blockData.view === "plot") { } else if (blockData.view === "plot") {
blockElem = <PlotView />; blockElem = <PlotView key={blockId} />;
} else if (blockData.view === "codeedit") { } else if (blockData.view === "codeedit") {
blockElem = <CodeEdit text={null} filename={null} />; blockElem = <CodeEdit key={blockId} text={null} filename={null} />;
} }
return ( return (
<BlockFrame <BlockFrame
key={blockId}
blockId={blockId} blockId={blockId}
onClose={onClose} onClose={onClose}
preview={false} preview={false}
onClick={() => setBlockClicked(true)} onClick={setBlockClickedTrue}
blockRef={blockRef} blockRef={blockRef}
dragHandleRef={dragHandleRef} dragHandleRef={dragHandleRef}
> >
@ -448,6 +456,6 @@ const Block = ({ blockId, onClose, dragHandleRef }: BlockProps) => {
</div> </div>
</BlockFrame> </BlockFrame>
); );
}; });
export { Block, BlockFrame }; export { Block, BlockFrame };

View File

@ -6,7 +6,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
className?: string; className?: string;
} }
const Button: React.FC<ButtonProps> = ({ className = "primary", children, disabled, ...props }) => { const Button: React.FC<ButtonProps> = React.memo(({ className = "primary", children, disabled, ...props }) => {
const hasIcon = React.Children.toArray(children).some( const hasIcon = React.Children.toArray(children).some(
(child) => React.isValidElement(child) && (child as React.ReactElement).type === "svg" (child) => React.isValidElement(child) && (child as React.ReactElement).type === "svg"
); );
@ -23,6 +23,6 @@ const Button: React.FC<ButtonProps> = ({ className = "primary", children, disabl
{children} {children}
</button> </button>
); );
}; });
export { Button }; export { Button };

View File

@ -6,6 +6,7 @@ import { ContextMenuModel } from "@/store/contextmenu";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import { clsx } from "clsx"; import { clsx } from "clsx";
import * as React from "react";
import { forwardRef, useEffect, useRef, useState } from "react"; import { forwardRef, useEffect, useRef, useState } from "react";
import "./tab.less"; import "./tab.less";
@ -22,7 +23,8 @@ interface TabProps {
onLoaded: () => void; onLoaded: () => void;
} }
const Tab = forwardRef<HTMLDivElement, TabProps>( const Tab = React.memo(
forwardRef<HTMLDivElement, TabProps>(
({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => { ({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => {
const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id)); const [tabData, tabLoading] = WOS.useWaveObjectValue<Tab>(WOS.makeORef("tab", id));
const [originalName, setOriginalName] = useState(""); const [originalName, setOriginalName] = useState("");
@ -145,6 +147,7 @@ const Tab = forwardRef<HTMLDivElement, TabProps>(
</div> </div>
); );
} }
)
); );
export { Tab }; export { Tab };

View File

@ -37,7 +37,7 @@ interface TabBarProps {
workspace: Workspace; workspace: Workspace;
} }
const TabBar = ({ workspace }: TabBarProps) => { const TabBar = React.memo(({ workspace }: TabBarProps) => {
const [tabIds, setTabIds] = useState<string[]>([]); const [tabIds, setTabIds] = useState<string[]>([]);
const [dragStartPositions, setDragStartPositions] = useState<number[]>([]); const [dragStartPositions, setDragStartPositions] = useState<number[]>([]);
const [draggingTab, setDraggingTab] = useState<string>(); const [draggingTab, setDraggingTab] = useState<string>();
@ -497,6 +497,6 @@ const TabBar = ({ workspace }: TabBarProps) => {
<WindowDrag ref={draggerRightRef} className="right" /> <WindowDrag ref={draggerRightRef} className="right" />
</div> </div>
); );
}; });
export { TabBar }; export { TabBar };

View File

@ -4,16 +4,17 @@
import { Block, BlockFrame } from "@/app/block/block"; import { Block, BlockFrame } from "@/app/block/block";
import * as services from "@/store/services"; import * as services from "@/store/services";
import * as WOS from "@/store/wos"; import * as WOS from "@/store/wos";
import * as React from "react";
import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems"; import { CenteredDiv, CenteredLoadingDiv } from "@/element/quickelems";
import { TileLayout } from "@/faraday/index"; import { TileLayout } from "@/faraday/index";
import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom"; import { getLayoutStateAtomForTab } from "@/faraday/lib/layoutAtom";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useMemo } from "react"; import { useMemo } from "react";
import { getApi } from "../store/global"; import { getApi } from "../store/global";
import "./tabcontent.less"; import "./tabcontent.less";
const TabContent = ({ tabId }: { tabId: string }) => { const TabContent = React.memo(({ tabId }: { tabId: string }) => {
const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]); const oref = useMemo(() => WOS.makeORef("tab", tabId), [tabId]);
const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]); const loadingAtom = useMemo(() => WOS.getWaveObjectLoadingAtom(oref), [oref]);
const tabLoading = useAtomValue(loadingAtom); const tabLoading = useAtomValue(loadingAtom);
@ -21,31 +22,40 @@ const TabContent = ({ tabId }: { tabId: string }) => {
const layoutStateAtom = useMemo(() => getLayoutStateAtomForTab(tabId, tabAtom), [tabAtom, tabId]); const layoutStateAtom = useMemo(() => getLayoutStateAtomForTab(tabId, tabAtom), [tabAtom, tabId]);
const tabData = useAtomValue(tabAtom); const tabData = useAtomValue(tabAtom);
const renderBlock = useCallback( const tileLayoutContents = useMemo(() => {
( function renderBlock(
tabData: TabLayoutData, tabData: TabLayoutData,
ready: boolean, ready: boolean,
onClose: () => void, onClose: () => void,
dragHandleRef: React.RefObject<HTMLDivElement> dragHandleRef: React.RefObject<HTMLDivElement>
) => { ) {
if (!tabData.blockId || !ready) { if (!tabData.blockId || !ready) {
return null; return null;
} }
return <Block blockId={tabData.blockId} onClose={onClose} dragHandleRef={dragHandleRef} />; return (
}, <Block
[] key={tabData.blockId}
blockId={tabData.blockId}
onClose={onClose}
dragHandleRef={dragHandleRef}
/>
); );
}
const renderPreview = useCallback((tabData: TabLayoutData) => { function renderPreview(tabData: TabLayoutData) {
return <BlockFrame blockId={tabData.blockId} preview={true} />; return <BlockFrame key={tabData.blockId} blockId={tabData.blockId} preview={true} />;
}, []); }
const onNodeDelete = useCallback((data: TabLayoutData) => { function onNodeDelete(data: TabLayoutData) {
return services.ObjectService.DeleteBlock(data.blockId); return services.ObjectService.DeleteBlock(data.blockId);
}, []); }
const getCursorPoint = useCallback(() => { return {
return getApi().getCursorPoint(); renderContent: renderBlock,
renderPreview: renderPreview,
tabId: tabId,
onNodeDelete: onNodeDelete,
};
}, []); }, []);
if (tabLoading) { if (tabLoading) {
@ -68,15 +78,12 @@ const TabContent = ({ tabId }: { tabId: string }) => {
<div className="tabcontent"> <div className="tabcontent">
<TileLayout <TileLayout
key={tabId} key={tabId}
tabId={tabId} contents={tileLayoutContents}
renderContent={renderBlock}
renderPreview={renderPreview}
layoutTreeStateAtom={layoutStateAtom} layoutTreeStateAtom={layoutStateAtom}
onNodeDelete={onNodeDelete} getCursorPoint={getApi().getCursorPoint}
getCursorPoint={getCursorPoint}
/> />
</div> </div>
); );
}; });
export { TabContent }; export { TabContent };

View File

@ -67,6 +67,13 @@
&.col-size { &.col-size {
text-align: right; text-align: right;
} }
.dir-table-lastmod,
.dir-table-modestr,
.dir-table-size,
.dir-table-type {
color: var(--secondary-text-color);
}
} }
} }
} }

View File

@ -158,37 +158,40 @@ function DirectoryTable({ data, cwd, setFileName }: DirectoryTableProps) {
() => [ () => [
columnHelper.accessor("mimetype", { columnHelper.accessor("mimetype", {
cell: (info) => <i className={getIconFromMimeType(info.getValue() ?? "")}></i>, cell: (info) => <i className={getIconFromMimeType(info.getValue() ?? "")}></i>,
header: () => <span></span>, header: () => <span>Type</span>,
id: "logo", id: "logo",
size: 25, size: 25,
enableSorting: false, enableSorting: false,
}), }),
columnHelper.accessor("path", { columnHelper.accessor("path", {
cell: (info) => info.getValue(), cell: (info) => <span className="dir-table-path">{info.getValue()}</span>,
header: () => <span>Name</span>, header: () => <span>Name</span>,
sortingFn: "alphanumeric", sortingFn: "alphanumeric",
}), }),
columnHelper.accessor("modestr", { columnHelper.accessor("modestr", {
cell: (info) => info.getValue(), cell: (info) => <span className="dir-table-modestr">{info.getValue()}</span>,
header: () => <span>Permissions</span>, header: () => <span>Permissions</span>,
size: 91, size: 91,
sortingFn: "alphanumeric", sortingFn: "alphanumeric",
}), }),
columnHelper.accessor("modtime", { columnHelper.accessor("modtime", {
cell: (info) => cell: (info) => (
getLastModifiedTime(info.getValue(), settings.datetime.locale, settings.datetime.format), <span className="dir-table-lastmod">
{getLastModifiedTime(info.getValue(), settings.datetime.locale, settings.datetime.format)}
</span>
),
header: () => <span>Last Modified</span>, header: () => <span>Last Modified</span>,
size: 185, size: 185,
sortingFn: "datetime", sortingFn: "datetime",
}), }),
columnHelper.accessor("size", { columnHelper.accessor("size", {
cell: (info) => getBestUnit(info.getValue()), cell: (info) => <span className="dir-table-size">{getBestUnit(info.getValue())}</span>,
header: () => <span>Size</span>, header: () => <span>Size</span>,
size: 55, size: 55,
sortingFn: "auto", sortingFn: "auto",
}), }),
columnHelper.accessor("mimetype", { columnHelper.accessor("mimetype", {
cell: (info) => info.getValue(), cell: (info) => <span className="dir-table-type">{info.getValue()}</span>,
header: () => <span>Type</span>, header: () => <span>Type</span>,
sortingFn: "alphanumeric", sortingFn: "alphanumeric",
}), }),

View File

@ -14,7 +14,7 @@ import "./workspace.less";
const iconRegex = /^[a-z0-9-]+$/; const iconRegex = /^[a-z0-9-]+$/;
function Widgets() { const Widgets = React.memo(() => {
const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom); const settingsConfig = jotai.useAtomValue(atoms.settingsConfigAtom);
const newWidgetModalVisible = React.useState(false); const newWidgetModalVisible = React.useState(false);
async function clickTerminal() { async function clickTerminal() {
@ -92,27 +92,27 @@ function Widgets() {
</div> </div>
</div> </div>
); );
} });
function WorkspaceElem() { const WorkspaceElem = React.memo(() => {
const windowData = jotai.useAtomValue(atoms.waveWindow); const windowData = jotai.useAtomValue(atoms.waveWindow);
const activeTabId = windowData?.activetabid; const activeTabId = windowData?.activetabid;
const ws = jotai.useAtomValue(atoms.workspace); const ws = jotai.useAtomValue(atoms.workspace);
return ( return (
<div className="workspace"> <div className="workspace">
<TabBar workspace={ws} /> <TabBar key={ws.oid} workspace={ws} />
<div className="workspace-tabcontent"> <div className="workspace-tabcontent">
{activeTabId == "" ? ( {activeTabId == "" ? (
<CenteredDiv>No Active Tab</CenteredDiv> <CenteredDiv>No Active Tab</CenteredDiv>
) : ( ) : (
<> <>
<TabContent key={windowData.workspaceid} tabId={activeTabId} /> <TabContent key={activeTabId} tabId={activeTabId} />
<Widgets /> <Widgets />
</> </>
)} )}
</div> </div>
</div> </div>
); );
} });
export { WorkspaceElem as Workspace }; export { WorkspaceElem as Workspace };

View File

@ -37,11 +37,11 @@ import {
import "./tilelayout.less"; import "./tilelayout.less";
import { Dimensions, FlexDirection, setTransform as createTransform, determineDropDirection } from "./utils"; import { Dimensions, FlexDirection, setTransform as createTransform, determineDropDirection } from "./utils";
export interface TileLayoutProps<T> {
/** /**
* The atom containing the layout tree state. * contains callbacks and information about the contents (or styling) of of the TileLayout
* nothing in here is specific to the TileLayout itself
*/ */
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>; export interface TileLayoutContents<T> {
/** /**
* A callback that accepts the data from the leaf node and displays the leaf contents to the user. * A callback that accepts the data from the leaf node and displays the leaf contents to the user.
*/ */
@ -64,6 +64,18 @@ export interface TileLayoutProps<T> {
* tabId this TileLayout is associated with * tabId this TileLayout is associated with
*/ */
tabId: string; tabId: string;
}
export interface TileLayoutProps<T> {
/**
* The atom containing the layout tree state.
*/
layoutTreeStateAtom: WritableLayoutTreeStateAtom<T>;
/**
* callbacks and information about the contents (or styling) of the TileLayout or contents
*/
contents: TileLayoutContents<T>;
/** /**
* A callback for getting the cursor point in reference to the current window. This removes Electron as a runtime dependency, allowing for better integration with Storybook. * A callback for getting the cursor point in reference to the current window. This removes Electron as a runtime dependency, allowing for better integration with Storybook.
@ -75,15 +87,7 @@ export interface TileLayoutProps<T> {
const DragPreviewWidth = 300; const DragPreviewWidth = 300;
const DragPreviewHeight = 300; const DragPreviewHeight = 300;
export const TileLayout = <T,>({ export const TileLayout = React.memo(<T,>({ layoutTreeStateAtom, contents, getCursorPoint }: TileLayoutProps<T>) => {
layoutTreeStateAtom,
tabId,
className,
renderContent,
renderPreview,
onNodeDelete,
getCursorPoint,
}: TileLayoutProps<T>) => {
const overlayContainerRef = useRef<HTMLDivElement>(null); const overlayContainerRef = useRef<HTMLDivElement>(null);
const displayContainerRef = useRef<HTMLDivElement>(null); const displayContainerRef = useRef<HTMLDivElement>(null);
@ -126,7 +130,7 @@ export const TileLayout = <T,>({
const [layoutLeafTransforms, setLayoutLeafTransformsRaw] = useState<Record<string, CSSProperties>>({}); const [layoutLeafTransforms, setLayoutLeafTransformsRaw] = useState<Record<string, CSSProperties>>({});
const setLayoutLeafTransforms = (transforms: Record<string, CSSProperties>) => { const setLayoutLeafTransforms = (transforms: Record<string, CSSProperties>) => {
globalLayoutTransformsMap.set(tabId, transforms); globalLayoutTransformsMap.set(contents.tabId, transforms);
setLayoutLeafTransformsRaw(transforms); setLayoutLeafTransformsRaw(transforms);
}; };
@ -247,30 +251,23 @@ export const TileLayout = <T,>({
// console.log("calling dispatch", deleteAction); // console.log("calling dispatch", deleteAction);
dispatch(deleteAction); dispatch(deleteAction);
// console.log("calling onNodeDelete", node); // console.log("calling onNodeDelete", node);
await onNodeDelete?.(node.data); await contents.onNodeDelete?.(node.data);
// console.log("node deleted"); // console.log("node deleted");
}, },
[onNodeDelete, dispatch] [contents.onNodeDelete, dispatch]
); );
return ( return (
<Suspense> <Suspense>
<div className={clsx("tile-layout", className, { animate })} onPointerOut={onPointerLeave}> <div className={clsx("tile-layout", contents.className, { animate })} onPointerOut={onPointerLeave}>
<div key="display" ref={displayContainerRef} className="display-container"> <div key="display" ref={displayContainerRef} className="display-container">
{layoutLeafTransforms && <DisplayNodesWrapper
layoutTreeState.leafs.map((leaf) => { contents={contents}
return (
<DisplayNode
key={leaf.id}
layoutNode={leaf}
renderContent={renderContent}
renderPreview={renderPreview}
transform={layoutLeafTransforms[leaf.id]}
onLeafClose={onLeafClose}
ready={animate} ready={animate}
onLeafClose={onLeafClose}
layoutTreeState={layoutTreeState}
layoutLeafTransforms={layoutLeafTransforms}
/> />
);
})}
</div> </div>
<Placeholder <Placeholder
key="placeholder" key="placeholder"
@ -296,21 +293,63 @@ export const TileLayout = <T,>({
</div> </div>
</Suspense> </Suspense>
); );
}; });
interface DisplayNodesWrapperProps<T> {
/**
* The layout tree state.
*/
layoutTreeState: LayoutTreeState<T>;
/**
* contains callbacks and information about the contents (or styling) of of the TileLayout
*/
contents: TileLayoutContents<T>;
/**
* A callback that is called when a leaf node gets closed.
* @param node The node that is closed.
*/
onLeafClose: (node: LayoutNode<T>) => void;
/**
* A series of CSS properties used to display a leaf node with the correct dimensions and position, as determined from its corresponding OverlayNode.
*/
layoutLeafTransforms: Record<string, CSSProperties>;
/**
* Determines whether the leaf nodes are ready to be displayed to the user.
*/
ready: boolean;
}
const DisplayNodesWrapper = React.memo(
<T,>({ layoutTreeState, contents, onLeafClose, layoutLeafTransforms, ready }: DisplayNodesWrapperProps<T>) => {
if (!layoutLeafTransforms) {
return null;
}
return layoutTreeState.leafs.map((leaf) => {
return (
<DisplayNode
key={leaf.id}
layoutNode={leaf}
contents={contents}
transform={layoutLeafTransforms[leaf.id]}
onLeafClose={onLeafClose}
ready={ready}
/>
);
});
}
);
interface DisplayNodeProps<T> { interface DisplayNodeProps<T> {
/** /**
* The leaf node object, containing the data needed to display the leaf contents to the user. * The leaf node object, containing the data needed to display the leaf contents to the user.
*/ */
layoutNode: LayoutNode<T>; layoutNode: LayoutNode<T>;
/** /**
* A callback that accepts the data from the leaf node and displays the leaf contents to the user. * contains callbacks and information about the contents (or styling) of of the TileLayout
*/ */
renderContent: ContentRenderer<T>; contents: TileLayoutContents<T>;
/**
* A callback that accepts the data from the leaf node and returns a preview that can be shown when the user drags a node.
*/
renderPreview?: PreviewRenderer<T>;
/** /**
* A callback that is called when a leaf node gets closed. * A callback that is called when a leaf node gets closed.
* @param node The node that is closed. * @param node The node that is closed.
@ -331,14 +370,7 @@ const dragItemType = "TILE_ITEM";
/** /**
* The draggable and displayable portion of a leaf node in a layout tree. * The draggable and displayable portion of a leaf node in a layout tree.
*/ */
const DisplayNode = <T,>({ const DisplayNode = React.memo(<T,>({ layoutNode, contents, transform, onLeafClose, ready }: DisplayNodeProps<T>) => {
layoutNode,
renderContent,
renderPreview,
transform,
onLeafClose,
ready,
}: DisplayNodeProps<T>) => {
const tileNodeRef = useRef<HTMLDivElement>(null); const tileNodeRef = useRef<HTMLDivElement>(null);
const dragHandleRef = useRef<HTMLDivElement>(null); const dragHandleRef = useRef<HTMLDivElement>(null);
const previewRef = useRef<HTMLDivElement>(null); const previewRef = useRef<HTMLDivElement>(null);
@ -370,11 +402,11 @@ const DisplayNode = <T,>({
transform: `scale(${1 / devicePixelRatio})`, transform: `scale(${1 / devicePixelRatio})`,
}} }}
> >
{renderPreview?.(layoutNode.data)} {contents.renderPreview?.(layoutNode.data)}
</div> </div>
</div> </div>
); );
}, [renderPreview, devicePixelRatio]); }, [contents.renderPreview, devicePixelRatio]);
const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null); const [previewImage, setPreviewImage] = useState<HTMLImageElement>(null);
const [previewImageGeneration, setPreviewImageGeneration] = useState(0); const [previewImageGeneration, setPreviewImageGeneration] = useState(0);
@ -414,7 +446,7 @@ const DisplayNode = <T,>({
return ( return (
layoutNode.data && ( layoutNode.data && (
<div key="leaf" className="tile-leaf"> <div key="leaf" className="tile-leaf">
{renderContent(layoutNode.data, ready, onClose, dragHandleRef)} {contents.renderContent(layoutNode.data, ready, onClose, dragHandleRef)}
</div> </div>
) )
); );
@ -436,7 +468,7 @@ const DisplayNode = <T,>({
{previewElement} {previewElement}
</div> </div>
); );
}; });
interface OverlayNodeProps<T> { interface OverlayNodeProps<T> {
/** /**