mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-02 18:39:05 +01:00
Fix block content sizing (#280)
Make the block content sizing update once when its node moves or becomes magnified. By manually updating this inner sizing rather than letting the block flow in the DOM, the animations of the block frames are much smoother. This also fixes an issue where two scrollbars were being rendered for the Directory Preview widget. This also sets zero padding on nodes when there's only a single node being rendered.
This commit is contained in:
parent
86fc45fc13
commit
c892c39a73
@ -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<Dimensions>();
|
||||
|
||||
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<React.CSSProperties>(() => {
|
||||
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={() => {}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
key="content"
|
||||
className="block-content"
|
||||
style={{
|
||||
pointerEvents: disablePointerEvents ? "none" : undefined,
|
||||
width: addlProps?.transform?.width,
|
||||
height: addlProps?.transform?.height,
|
||||
}}
|
||||
ref={contentRef}
|
||||
>
|
||||
<div key="content" className="block-content" ref={contentRef} style={blockContentStyle}>
|
||||
<ErrorBoundary>
|
||||
<React.Suspense fallback={<CenteredDiv>Loading...</CenteredDiv>}>{viewElem}</React.Suspense>
|
||||
</ErrorBoundary>
|
||||
|
@ -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 <FrameElem {...props} numBlocksInTab={numBlocks} />;
|
||||
});
|
||||
|
@ -11,7 +11,7 @@
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 3px 3px 3px 0;
|
||||
padding-right: 3px;
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
|
@ -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 {
|
||||
|
@ -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 (
|
||||
|
@ -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<string[]>;
|
||||
/**
|
||||
* Atom representing the number of leaf nodes in a layout.
|
||||
*/
|
||||
numLeafs: Atom<number>;
|
||||
/**
|
||||
* A map of node models for currently-active leafs.
|
||||
*/
|
||||
@ -197,7 +207,8 @@ export class LayoutModel {
|
||||
renderContent?: ContentRenderer,
|
||||
renderPreview?: PreviewRenderer,
|
||||
onNodeDelete?: (data: TabLayoutData) => Promise<void>,
|
||||
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;
|
||||
|
@ -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<Tab>, 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<NodeJS.Timeout>();
|
||||
const [innerRect, setInnerRect] = useState<CSSProperties>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -316,9 +316,12 @@ export interface LayoutNodeAdditionalProps {
|
||||
|
||||
export interface NodeModel {
|
||||
additionalProps: Atom<LayoutNodeAdditionalProps>;
|
||||
animationTimeS: number;
|
||||
innerRect: Atom<CSSProperties>;
|
||||
blockNum: Atom<number>;
|
||||
nodeId: string;
|
||||
blockId: string;
|
||||
isResizing: Atom<boolean>;
|
||||
isFocused: Atom<boolean>;
|
||||
isMagnified: Atom<boolean>;
|
||||
ready: Atom<boolean>;
|
||||
|
Loading…
Reference in New Issue
Block a user