diff --git a/frontend/app/block/block.tsx b/frontend/app/block/block.tsx index 846cd0e6b..e1d46992d 100644 --- a/frontend/app/block/block.tsx +++ b/frontend/app/block/block.tsx @@ -6,7 +6,7 @@ import { PlotView } from "@/app/view/plotview/plotview"; import { PreviewModel, PreviewView, makePreviewModel } from "@/app/view/preview/preview"; import { ErrorBoundary } from "@/element/errorboundary"; import { CenteredDiv } from "@/element/quickelems"; -import { NodeModel } from "@/layout/index"; +import { NodeModel, useDebouncedNodeInnerRect } from "@/layout/index"; import { counterInc, getViewModel, registerViewModel, unregisterViewModel } from "@/store/global"; import * as WOS from "@/store/wos"; import * as util from "@/util/util"; @@ -122,7 +122,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { const [focusedChild, setFocusedChild] = React.useState(null); const isFocused = jotai.useAtomValue(nodeModel.isFocused); const disablePointerEvents = jotai.useAtomValue(nodeModel.disablePointerEvents); - const addlProps = jotai.useAtomValue(nodeModel.additionalProps); + const innerRect = useDebouncedNodeInnerRect(nodeModel); React.useLayoutEffect(() => { setBlockClicked(isFocused); @@ -152,6 +152,32 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { setBlockClicked(true); }, []); + const [blockContentOffset, setBlockContentOffset] = React.useState(); + + React.useEffect(() => { + if (blockRef.current && contentRef.current) { + const blockRect = blockRef.current.getBoundingClientRect(); + const contentRect = contentRef.current.getBoundingClientRect(); + setBlockContentOffset({ + top: 0, + left: 0, + width: blockRect.width - contentRect.width, + height: blockRect.height - contentRect.height, + }); + } + }, [blockRef, contentRef]); + + const blockContentStyle = React.useMemo(() => { + const retVal: React.CSSProperties = { + pointerEvents: disablePointerEvents ? "none" : undefined, + }; + if (innerRect?.width && innerRect.height && blockContentOffset) { + retVal.width = `calc(${innerRect?.width} - ${blockContentOffset.width}px)`; + retVal.height = `calc(${innerRect?.height} - ${blockContentOffset.height}px)`; + } + return retVal; + }, [innerRect, disablePointerEvents, blockContentOffset]); + const viewElem = React.useMemo( () => getViewElem(nodeModel.blockId, blockData?.meta?.view, viewModel), [nodeModel.blockId, blockData?.meta?.view, viewModel] @@ -195,16 +221,7 @@ const BlockFull = React.memo(({ nodeModel, viewModel }: FullBlockProps) => { onChange={() => {}} /> -
+
Loading...}>{viewElem} diff --git a/frontend/app/block/blockframe.tsx b/frontend/app/block/blockframe.tsx index a6041ef29..8c1c87adc 100644 --- a/frontend/app/block/blockframe.tsx +++ b/frontend/app/block/blockframe.tsx @@ -402,7 +402,7 @@ const BlockFrame = React.memo((props: BlockFrameProps) => { if (!blockId || !blockData) { return null; } - let FrameElem = BlockFrame_Default; + const FrameElem = BlockFrame_Default; const numBlocks = tabData?.blockids?.length ?? 0; return ; }); diff --git a/frontend/app/tab/tabcontent.less b/frontend/app/tab/tabcontent.less index e7ada760a..6ab838625 100644 --- a/frontend/app/tab/tabcontent.less +++ b/frontend/app/tab/tabcontent.less @@ -11,7 +11,7 @@ justify-content: center; overflow: hidden; position: relative; - padding: 3px 3px 3px 0; + padding-right: 3px; .block-container { display: flex; diff --git a/frontend/layout/index.ts b/frontend/layout/index.ts index 09035f1bc..57ce26312 100644 --- a/frontend/layout/index.ts +++ b/frontend/layout/index.ts @@ -8,6 +8,7 @@ import { getLayoutModelForActiveTab, getLayoutModelForTab, getLayoutModelForTabById, + useDebouncedNodeInnerRect, useLayoutModel, } from "./lib/layoutModelHooks"; import { newLayoutNode } from "./lib/layoutNode"; @@ -44,6 +45,7 @@ export { NavigateDirection, newLayoutNode, TileLayout, + useDebouncedNodeInnerRect, useLayoutModel, }; export type { diff --git a/frontend/layout/lib/TileLayout.tsx b/frontend/layout/lib/TileLayout.tsx index 36d23e31d..3d9a2906a 100644 --- a/frontend/layout/lib/TileLayout.tsx +++ b/frontend/layout/lib/TileLayout.tsx @@ -102,8 +102,12 @@ function TileLayoutComponent({ tabAtom, contents, getCursorPoint }: TileLayoutPr }, []); const tileStyle = useMemo( - () => ({ "--gap-size-px": `${layoutModel.gapSizePx}px` }) as CSSProperties, - [layoutModel.gapSizePx] + () => + ({ + "--gap-size-px": `${layoutModel.gapSizePx}px`, + "--animation-time-s": `${layoutModel.animationTimeS}s`, + }) as CSSProperties, + [layoutModel.gapSizePx, layoutModel.animationTimeS] ); return ( diff --git a/frontend/layout/lib/layoutModel.ts b/frontend/layout/lib/layoutModel.ts index 7d82272f5..447ab0e3a 100644 --- a/frontend/layout/lib/layoutModel.ts +++ b/frontend/layout/lib/layoutModel.ts @@ -55,6 +55,7 @@ interface ResizeContext { const DefaultGapSizePx = 5; const MinNodeSizePx = 40; +const DefaultAnimationTimeS = 0.15; export class LayoutModel { /** @@ -94,6 +95,11 @@ export class LayoutModel { */ gapSizePx: number; + /** + * The time a transition animation takes, in seconds. + */ + animationTimeS: number; + /** * List of nodes that are leafs and should be rendered as a DisplayNode. */ @@ -102,6 +108,10 @@ export class LayoutModel { * An ordered list of node ids starting from the top left corner to the bottom right corner. */ leafOrder: PrimitiveAtom; + /** + * Atom representing the number of leaf nodes in a layout. + */ + numLeafs: Atom; /** * A map of node models for currently-active leafs. */ @@ -197,7 +207,8 @@ export class LayoutModel { renderContent?: ContentRenderer, renderPreview?: PreviewRenderer, onNodeDelete?: (data: TabLayoutData) => Promise, - gapSizePx?: number + gapSizePx?: number, + animationTimeS?: number ) { this.treeStateAtom = treeStateAtom; this.getter = getter; @@ -208,9 +219,11 @@ export class LayoutModel { this.gapSizePx = gapSizePx ?? DefaultGapSizePx; this.halfResizeHandleSizePx = this.gapSizePx > 5 ? this.gapSizePx : DefaultGapSizePx; this.resizeHandleSizePx = 2 * this.halfResizeHandleSizePx; + this.animationTimeS = animationTimeS ?? DefaultAnimationTimeS; this.leafs = atom([]); this.leafOrder = atom([]); + this.numLeafs = atom((get) => get(this.leafOrder).length); this.nodeModels = new Map(); this.additionalProps = atom({}); @@ -639,12 +652,27 @@ export class LayoutModel { getNodeModel(node: LayoutNode): NodeModel { const nodeid = node.id; const blockId = node.data.blockId; + const addlPropsAtom = this.getNodeAdditionalPropertiesAtom(nodeid); if (!this.nodeModels.has(nodeid)) { this.nodeModels.set(nodeid, { - additionalProps: this.getNodeAdditionalPropertiesAtom(nodeid), + additionalProps: addlPropsAtom, + animationTimeS: this.animationTimeS, + innerRect: atom((get) => { + const addlProps = get(addlPropsAtom); + const numLeafs = get(this.numLeafs); + if (numLeafs > 1 && addlProps?.rect) { + return { + width: `${addlProps.transform.width} - ${this.gapSizePx}px`, + height: `${addlProps.transform.height} - ${this.gapSizePx}px`, + } as CSSProperties; + } else { + return null; + } + }), nodeId: nodeid, blockId, blockNum: atom((get) => get(this.leafOrder).indexOf(nodeid) + 1), + isResizing: this.isResizing, isFocused: atom((get) => { const treeState = get(this.treeStateAtom); const isFocused = treeState.focusedNodeId === nodeid; diff --git a/frontend/layout/lib/layoutModelHooks.ts b/frontend/layout/lib/layoutModelHooks.ts index 2190ab2f4..27927de14 100644 --- a/frontend/layout/lib/layoutModelHooks.ts +++ b/frontend/layout/lib/layoutModelHooks.ts @@ -4,7 +4,7 @@ import { atoms, globalStore, WOS } from "@/app/store/global"; import useResizeObserver from "@react-hook/resize-observer"; import { Atom, useAtomValue } from "jotai"; -import { useEffect } from "react"; +import { CSSProperties, useEffect, useState } from "react"; import { withLayoutTreeStateAtomFromTab } from "./layoutAtom"; import { LayoutModel } from "./layoutModel"; import { LayoutNode, NodeModel, TileLayoutContents } from "./types"; @@ -59,3 +59,30 @@ export function useTileLayout(tabAtom: Atom, tileContent: TileLayoutContent export function useNodeModel(layoutModel: LayoutModel, layoutNode: LayoutNode): NodeModel { return layoutModel.getNodeModel(layoutNode); } + +export function useDebouncedNodeInnerRect(nodeModel: NodeModel): CSSProperties { + const nodeInnerRect = useAtomValue(nodeModel.innerRect); + const nodeIsResizing = useAtomValue(nodeModel.isResizing); + + const [debounceTimeout, setDebounceTimeout] = useState(); + const [innerRect, setInnerRect] = useState(); + + useEffect(() => { + if (!nodeIsResizing && nodeInnerRect) { + if (debounceTimeout) { + clearTimeout(debounceTimeout); + } + setDebounceTimeout( + setTimeout(() => { + console.log("setting inner rect", nodeInnerRect); + setInnerRect(nodeInnerRect); + setDebounceTimeout(null); + }, nodeModel.animationTimeS * 1000) + ); + } else { + setInnerRect(null); + } + }, [nodeInnerRect, nodeIsResizing]); + + return innerRect; +} diff --git a/frontend/layout/lib/tilelayout.less b/frontend/layout/lib/tilelayout.less index 05c20fad0..570ad371a 100644 --- a/frontend/layout/lib/tilelayout.less +++ b/frontend/layout/lib/tilelayout.less @@ -99,12 +99,20 @@ border: 1px solid var(--accent-color); backdrop-filter: blur(8px); } + + .tile-leaf { + overflow: hidden; + } + + &:not(:only-child) .tile-leaf { + padding: calc(var(--gap-size-px) / 2); + } } &.animate { .tile-node, .placeholder { - transition-duration: 0.15s; + transition-duration: var(--animation-time-s); transition-timing-function: ease-in; transition-property: transform, width, height, background-color; } @@ -116,11 +124,6 @@ width: 100%; } - .tile-leaf { - overflow: hidden; - padding: calc(var(--gap-size-px) / 2); - } - .placeholder { background-color: var(--accent-color); opacity: 0.5; diff --git a/frontend/layout/lib/types.ts b/frontend/layout/lib/types.ts index b649ddbec..64307f5c1 100644 --- a/frontend/layout/lib/types.ts +++ b/frontend/layout/lib/types.ts @@ -316,9 +316,12 @@ export interface LayoutNodeAdditionalProps { export interface NodeModel { additionalProps: Atom; + animationTimeS: number; + innerRect: Atom; blockNum: Atom; nodeId: string; blockId: string; + isResizing: Atom; isFocused: Atom; isMagnified: Atom; ready: Atom;