feat(ui): layer reordering styling

This commit is contained in:
psychedelicious
2024-10-30 21:39:08 +10:00
parent 54abd8d4d1
commit 57122c6aa3
5 changed files with 80 additions and 103 deletions

View File

@@ -4,14 +4,14 @@ import { draggable, dropTargetForElements } from '@atlaskit/pragmatic-drag-and-d
// import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
import { attachClosestEdge, extractClosestEdge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
import { Flex } from '@invoke-ai/ui-library';
import { Box, Flex } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityIsSelected } from 'features/controlLayers/hooks/useEntityIsSelected';
import { useEntitySelectionColor } from 'features/controlLayers/hooks/useEntitySelectionColor';
import { entitySelected } from 'features/controlLayers/store/canvasSlice';
import { Dnd } from 'features/dnd/dnd';
import DropIndicator from 'features/dnd/DndDropIndicator';
import { DndDropIndicator } from 'features/dnd/DndDropIndicator';
import type { PropsWithChildren } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
@@ -80,11 +80,6 @@ export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
dropTargetForElements({
element,
canDrop({ source }) {
// not allowing dropping on yourself
if (source.element === element) {
return false;
}
// only allowing tasks to be dropped on me
if (!Dnd.Source.singleCanvasEntity.typeGuard(source.data)) {
return false;
}
@@ -131,22 +126,29 @@ export const CanvasEntityContainer = memo((props: PropsWithChildren) => {
}, [entityIdentifier]);
return (
<Flex
ref={ref}
position="relative"
flexDir="column"
w="full"
bg={isSelected ? 'base.800' : 'base.850'}
onClick={onClick}
borderInlineStartWidth={5}
borderColor={isSelected ? selectionColor : 'base.800'}
borderRadius="base"
>
{props.children}
<Box position="relative">
<Flex
data-entity-id={entityIdentifier.id}
ref={ref}
position="relative"
flexDir="column"
w="full"
bg={isSelected ? 'base.800' : 'base.850'}
onClick={onClick}
borderInlineStartWidth={5}
borderColor={isSelected ? selectionColor : 'base.800'}
borderRadius="base"
>
{props.children}
</Flex>
{dndState.type === 'is-dragging-over' && dndState.closestEdge ? (
<DropIndicator edge={dndState.closestEdge} gap="8px" />
<DndDropIndicator
edge={dndState.closestEdge}
// This is the gap between items in the list
gap="var(--invoke-space-2)"
/>
) : null}
</Flex>
</Box>
);
});

View File

@@ -6,6 +6,7 @@ import { Button, Collapse, Flex, Icon, Spacer, Text } from '@invoke-ai/ui-librar
import { useAppDispatch } from 'app/store/storeHooks';
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
import { useBoolean } from 'common/hooks/useBoolean';
import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar';
import { fixTooltipCloseOnScrollStyles } from 'common/util/fixTooltipCloseOnScrollStyles';
import { CanvasEntityAddOfTypeButton } from 'features/controlLayers/components/common/CanvasEntityAddOfTypeButton';
import { CanvasEntityMergeVisibleButton } from 'features/controlLayers/components/common/CanvasEntityMergeVisibleButton';
@@ -15,7 +16,7 @@ import { useEntityTypeTitle } from 'features/controlLayers/hooks/useEntityTypeTi
import { entitiesReordered } from 'features/controlLayers/store/canvasSlice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { isRenderableEntityType } from 'features/controlLayers/store/types';
import { Dnd } from 'features/dnd/dnd';
import { Dnd, triggerPostMoveFlash } from 'features/dnd/dnd';
import type { PropsWithChildren } from 'react';
import { memo, useEffect } from 'react';
import { flushSync } from 'react-dom';
@@ -75,8 +76,29 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
return;
}
// Don't move if the source and target are the same index, meaning same position in the list
if (indexOfSource === indexOfTarget) {
return;
}
const closestEdgeOfTarget = extractClosestEdge(targetData);
// It's possible that the indices are different, but refer to the same position. For example, if the source is
// at 2 and the target is at 3, but the target edge is 'top', then the entity is already in the correct position.
// We should bail if this is the case.
let edgeIndexDelta = 0;
if (closestEdgeOfTarget === 'bottom') {
edgeIndexDelta = 1;
} else if (closestEdgeOfTarget === 'top') {
edgeIndexDelta = -1;
}
// If the source is already in the correct position, we don't need to move it.
if (indexOfSource === indexOfTarget + edgeIndexDelta) {
return;
}
// Using `flushSync` so we can query the DOM straight after this line
flushSync(() => {
dispatch(
@@ -92,14 +114,12 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children, entityI
})
);
});
// // Being simple and just querying for the task after the drop.
// // We could use react context to register the element in a lookup,
// // and then we could retrieve that element after the drop and use
// // `triggerPostMoveFlash`. But this gets the job done.
// const element = document.querySelector(`[data-task-id="${sourceData.taskId}"]`);
// if (element instanceof HTMLElement) {
// triggerPostMoveFlash(element);
// }
// Flash the element that was moved
const element = document.querySelector(`[data-entity-id="${sourceData.payload.entityIdentifier.id}"]`);
if (element instanceof HTMLElement) {
triggerPostMoveFlash(element, colorTokenToCssVar('base.700'));
}
},
});
}, [dispatch, entityIdentifiers, type]);

View File

@@ -552,6 +552,10 @@ export const getEntityIdentifier = <T extends CanvasEntityType>(
return { id: entity.id, type: entity.type };
};
export const entityIdentifierToString = (entityIdentifer: CanvasEntityIdentifier): string => {
return `${entityIdentifer.type}-${entityIdentifer.id}`;
};
export const isMaskEntityIdentifier = (
entityIdentifier: CanvasEntityIdentifier
): entityIdentifier is CanvasEntityIdentifier<'inpaint_mask' | 'regional_guidance'> => {

View File

@@ -1,20 +1,17 @@
/**
* Spacing tokens don't make a lot of sense for this specific use case,
* so disabling the linting rule.
*/
// Adapted from https://github.com/alexreardon/pdnd-react-tailwind/blob/main/src/drop-indicator.tsx
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/types';
import { Box, type SystemStyleObject } from '@invoke-ai/ui-library';
import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { Box } from '@invoke-ai/ui-library';
import type { CSSProperties } from 'react';
/**
* Design decisions for the drop indicator's main line
*/
export const line = {
borderRadius: 0,
thickness: 2,
backgroundColor: 'base.300',
backgroundColor: 'base.500',
};
export type DropIndicatorProps = {
/**
* The `edge` to draw a drop indicator on.
@@ -37,72 +34,30 @@ export type DropIndicatorProps = {
*/
gap?: string;
};
const terminalSize = 8;
const lineStyles: SystemStyleObject = {
display: 'block',
position: 'absolute',
zIndex: 1,
borderRadius: 'full',
// Blocking pointer events to prevent the line from triggering drag events
// Dragging over the line should count as dragging over the element behind it
pointerEvents: 'none',
background: line.backgroundColor,
// Terminal
'::before': {
content: '""',
width: terminalSize,
height: terminalSize,
boxSizing: 'border-box',
position: 'absolute',
border: `${line.thickness}px solid ${line.backgroundColor}`,
borderRadius: '50%',
},
};
/**
* By default, the edge of the terminal will be aligned to the edge of the line.
*
* Offsetting the terminal by half its size aligns the middle of the terminal
* with the edge of the line.
*
* We must offset by half the line width in the opposite direction so that the
* middle of the terminal aligns with the middle of the line.
*
* That is,
*
* offset = - (terminalSize / 2) + (line.thickness / 2)
*
* which simplifies to the following value.
*/
const offsetToAlignTerminalWithLine = (line.thickness - terminalSize) / 2;
/**
* We inset the line by half the terminal size,
* so that the terminal only half sticks out past the item.
*/
const lineOffset = terminalSize / 2;
type Orientation = 'horizontal' | 'vertical';
const orientationStyles: Record<Orientation, SystemStyleObject> = {
horizontal: {
height: line.thickness,
left: lineOffset,
right: 0,
'::before': {
// Horizontal indicators have the terminal on the left
left: -terminalSize,
},
height: `${line.thickness}px`,
left: 2,
right: 2,
},
vertical: {
width: line.thickness,
top: lineOffset,
bottom: 0,
'::before': {
// Vertical indicators have the terminal at the top
top: -terminalSize,
},
width: `${line.thickness}px`,
top: 2,
bottom: 2,
},
};
@@ -116,27 +71,15 @@ const edgeToOrientationMap: Record<Edge, Orientation> = {
const edgeStyles: Record<Edge, SystemStyleObject> = {
top: {
top: 'var(--local-line-offset)',
'::before': {
top: offsetToAlignTerminalWithLine,
},
},
right: {
right: 'var(--local-line-offset)',
'::before': {
right: offsetToAlignTerminalWithLine,
},
},
bottom: {
bottom: 'var(--local-line-offset)',
'::before': {
bottom: offsetToAlignTerminalWithLine,
},
},
left: {
left: 'var(--local-line-offset)',
'::before': {
left: offsetToAlignTerminalWithLine,
},
},
};
@@ -145,7 +88,7 @@ const edgeStyles: Record<Edge, SystemStyleObject> = {
*
* A drop indicator is used to communicate the intended resting place of the draggable item. The orientation of the drop indicator should always match the direction of the content flow.
*/
export function DropIndicator({ edge, gap = '0px' }: DropIndicatorProps) {
export function DndDropIndicator({ edge, gap = '0px' }: DropIndicatorProps) {
/**
* To clearly communicate the resting place of a draggable item during a drag operation,
* the drop indicator should be positioned half way between draggable items.
@@ -161,6 +104,3 @@ export function DropIndicator({ edge, gap = '0px' }: DropIndicatorProps) {
/>
);
}
// This default export is intended for usage with React.lazy
export default DropIndicator;

View File

@@ -6,6 +6,7 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library';
import { getPrefixedId } from 'features/controlLayers/konva/util';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import type { BoardId } from 'features/gallery/store/types';
import type { CSSProperties } from 'react';
import type { ImageDTO } from 'services/api/types';
import type { ValueOf } from 'type-fest';
import type { Jsonifiable } from 'type-fest/source/jsonifiable';
@@ -517,3 +518,13 @@ export function preserveOffsetOnSourceFallbackCentered({
return { x: offsetX, y: offsetY };
};
}
// Based on https://github.com/atlassian/pragmatic-drag-and-drop/blob/main/packages/flourish/src/trigger-post-move-flash.tsx
// That package has a lot of extra deps so we just copied the function here
export function triggerPostMoveFlash(element: HTMLElement, backgroundColor: CSSProperties['backgroundColor']) {
element.animate([{ backgroundColor }, {}], {
duration: 700,
easing: 'cubic-bezier(0.25, 0.1, 0.25, 1.0)',
iterations: 1,
});
}