Fix flicker when dragging a node through a node gap (#231)

This simplifies the rect calculations by making the gap between pixels
into a padding applied directly to the leaf nodes. This means the gaps
won't be present in the overlay layer, so when dragging a node around it
is always over an OverlayNode and the pendingAction won't be unset.

Also simplifies onDrop handling
This commit is contained in:
Evan Simkowitz 2024-08-15 14:53:13 -07:00 committed by GitHub
parent 9fa11ff838
commit c187059c8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 66 additions and 47 deletions

View File

@ -236,7 +236,7 @@ const DisplayNode = ({ layoutModel, layoutNode, contents }: DisplayNodeProps) =>
layoutNode.data,
]);
// Register the tile item as a draggable component
// Register the display node as a draggable item
useEffect(() => {
drag(dragHandleRef);
}, [drag, dragHandleRef.current]);
@ -320,7 +320,6 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
const additionalProps = useLayoutNode(layoutModel, layoutNode);
const overlayRef = useRef<HTMLDivElement>(null);
const generation = useAtomValue(layoutModel.generationAtom);
const pendingAction = useAtomValue(layoutModel.pendingAction.throttledValueAtom);
const [, drop] = useDrop(
() => ({
@ -333,27 +332,24 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
return false;
},
drop: (_, monitor) => {
// console.log("drop start", layoutNode.id, layoutTreeState.pendingAction);
if (!monitor.didDrop() && pendingAction) {
layoutModel.treeReducer({
type: LayoutTreeActionType.CommitPendingAction,
});
if (!monitor.didDrop()) {
layoutModel.onDrop();
}
},
hover: throttle(30, (_, monitor: DropTargetMonitor<unknown, unknown>) => {
hover: throttle(50, (_, monitor: DropTargetMonitor<unknown, unknown>) => {
if (monitor.isOver({ shallow: true })) {
if (monitor.canDrop() && layoutModel.displayContainerRef?.current) {
if (monitor.canDrop() && layoutModel.displayContainerRef?.current && additionalProps?.rect) {
const dragItem = monitor.getItem<LayoutNode>();
// console.log("computing operation", layoutNode, dragItem, additionalProps.rect);
const offset = monitor.getClientOffset();
const containerRect = layoutModel.displayContainerRef.current?.getBoundingClientRect();
offset.x -= containerRect?.x;
offset.y -= containerRect?.y;
const containerRect = layoutModel.displayContainerRef.current.getBoundingClientRect();
offset.x -= containerRect.x;
offset.y -= containerRect.y;
layoutModel.treeReducer({
type: LayoutTreeActionType.ComputeMove,
node: layoutNode,
nodeToMove: dragItem,
direction: determineDropDirection(additionalProps?.rect, offset),
direction: determineDropDirection(additionalProps.rect, offset),
} as LayoutTreeComputeMoveNodeAction);
} else {
layoutModel.treeReducer({
@ -363,10 +359,10 @@ const OverlayNode = ({ layoutNode, layoutModel }: OverlayNodeProps) => {
}
}),
}),
[layoutNode, pendingAction, generation, additionalProps, layoutModel.displayContainerRef]
[layoutNode, generation, additionalProps, layoutModel.displayContainerRef]
);
// Register the tile item as a draggable component
// Register the overlay node as a drop target
useEffect(() => {
drop(overlayRef);
}, []);

View File

@ -131,6 +131,18 @@ export class LayoutModel {
*/
lastMagnifiedNodeId: string;
/**
* The size of the resize handles, in CSS pixels.
* The resize handle size is double the gap size, or double the default gap size, whichever is greater.
* @see gapSizePx @see DefaultGapSizePx
*/
private resizeHandleSizePx: number;
/**
* Half of the size of the resize handles, in CSS pixels.
*
* @see resizeHandleSizePx This is just a precomputed halving of the resize handle size.
*/
private halfResizeHandleSizePx: number;
/**
* A context used by the resize handles to keep track of precomputed values for the current resize operation.
*/
@ -165,6 +177,8 @@ export class LayoutModel {
this.renderPreview = renderPreview;
this.onNodeDelete = onNodeDelete;
this.gapSizePx = gapSizePx ?? DefaultGapSizePx;
this.halfResizeHandleSizePx = this.gapSizePx > 5 ? this.gapSizePx : DefaultGapSizePx;
this.resizeHandleSizePx = 2 * this.halfResizeHandleSizePx;
this.leafs = [];
this.additionalProps = atom({});
@ -209,7 +223,7 @@ export class LayoutModel {
this.pendingAction = atomWithThrottle<LayoutTreeAction>(null, 10);
this.placeholderTransform = atom<CSSProperties>((get: Getter) => {
const pendingAction = get(this.pendingAction.throttledValueAtom);
console.log("update to pending action", pendingAction);
// console.log("update to pending action", pendingAction);
return this.getPlaceholderTransform(pendingAction);
});
@ -232,7 +246,7 @@ export class LayoutModel {
* @param action The action to perform.
*/
treeReducer(action: LayoutTreeAction) {
console.log("treeReducer", action, this);
// console.log("treeReducer", action, this);
let stateChanged = false;
switch (action.type) {
case LayoutTreeActionType.ComputeMove:
@ -399,24 +413,17 @@ export class LayoutModel {
const nodeRect: Dimensions = node.id === this.treeState.rootNode.id ? getBoundingRect() : additionalProps.rect;
const nodeIsRow = node.flexDirection === FlexDirection.Row;
const nodePixelsMinusGap =
(nodeIsRow ? nodeRect.width : nodeRect.height) - this.gapSizePx * (node.children.length - 1);
const nodePixels = nodeIsRow ? nodeRect.width : nodeRect.height;
const totalChildrenSize = node.children.reduce((acc, child) => acc + getNodeSize(child), 0);
const pixelToSizeRatio = totalChildrenSize / nodePixelsMinusGap;
const pixelToSizeRatio = totalChildrenSize / nodePixels;
let lastChildRect: Dimensions;
const resizeHandles: ResizeHandleProps[] = [];
node.children.forEach((child, i) => {
for (const child of node.children) {
const childSize = getNodeSize(child);
const rect: Dimensions = {
top:
!nodeIsRow && lastChildRect
? lastChildRect.top + lastChildRect.height + this.gapSizePx
: nodeRect.top,
left:
nodeIsRow && lastChildRect
? lastChildRect.left + lastChildRect.width + this.gapSizePx
: nodeRect.left,
top: !nodeIsRow && lastChildRect ? lastChildRect.top + lastChildRect.height : nodeRect.top,
left: nodeIsRow && lastChildRect ? lastChildRect.left + lastChildRect.width : nodeRect.left,
width: nodeIsRow ? childSize / pixelToSizeRatio : nodeRect.width,
height: nodeIsRow ? nodeRect.height : childSize / pixelToSizeRatio,
};
@ -426,24 +433,32 @@ export class LayoutModel {
transform,
};
// We only want the resize handles in between nodes, this ensures we have n-1 handles.
if (lastChildRect) {
const resizeHandleIndex = resizeHandles.length;
const resizeHandleDimensions: Dimensions = {
top: nodeIsRow ? rect.top : rect.top + rect.height - 0.5 * this.gapSizePx,
left: nodeIsRow ? rect.left + rect.width - 0.5 * this.gapSizePx : rect.left,
width: nodeIsRow ? 2 * this.gapSizePx : rect.width,
height: nodeIsRow ? rect.height : 2 * this.gapSizePx,
top: nodeIsRow
? lastChildRect.top
: lastChildRect.top + lastChildRect.height - this.halfResizeHandleSizePx,
left: nodeIsRow
? lastChildRect.left + lastChildRect.width - this.halfResizeHandleSizePx
: lastChildRect.left,
width: nodeIsRow ? this.resizeHandleSizePx : lastChildRect.width,
height: nodeIsRow ? lastChildRect.height : this.resizeHandleSizePx,
};
resizeHandles.push({
id: `${node.id}-${i}`,
id: `${node.id}-${resizeHandleIndex}`,
parentNodeId: node.id,
parentIndex: i,
parentIndex: resizeHandleIndex,
transform: setTransform(resizeHandleDimensions, true, false),
flexDirection: node.flexDirection,
centerPx: (nodeIsRow ? resizeHandleDimensions.left : resizeHandleDimensions.top) + this.gapSizePx,
centerPx:
(nodeIsRow ? resizeHandleDimensions.left : resizeHandleDimensions.top) +
this.halfResizeHandleSizePx,
});
}
lastChildRect = rect;
});
resizeHandles.pop();
}
additionalPropsMap[node.id] = {
...additionalProps,
@ -575,6 +590,14 @@ export class LayoutModel {
await this.onNodeDelete?.(node.data);
}
onDrop() {
if (this.getter(this.pendingAction.currentValueAtom)) {
this.treeReducer({
type: LayoutTreeActionType.CommitPendingAction,
});
}
}
/**
* Callback that is invoked when the TileLayout container is being resized.
*/

View File

@ -69,7 +69,6 @@
overflow: hidden;
width: 100%;
height: 100%;
z-index: inherit;
&.dragging {
filter: blur(8px);
@ -119,6 +118,7 @@
.tile-leaf {
overflow: hidden;
padding: calc(var(--gap-size-px) / 2);
}
.placeholder {