mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
feat(ui): layer reordering styling
This commit is contained in:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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'> => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user