mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): collapsible layers
This commit is contained in:
parent
cf1883585d
commit
d74cd12aa6
3
invokeai/frontend/web/src/common/util/stopPropagation.ts
Normal file
3
invokeai/frontend/web/src/common/util/stopPropagation.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const stopPropagation = (e: React.MouseEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import CALayerOpacity from 'features/controlLayers/components/CALayerOpacity';
|
import CALayerOpacity from 'features/controlLayers/components/CALayerOpacity';
|
||||||
@ -38,6 +38,8 @@ export const CALayerListItem = memo(({ layerId }: Props) => {
|
|||||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
// Must be capture so that the layer is selected before deleting/resetting/etc
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
|
const { isOpen, onToggle } = useDisclosure();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
gap={2}
|
gap={2}
|
||||||
@ -47,8 +49,8 @@ export const CALayerListItem = memo(({ layerId }: Props) => {
|
|||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
py="1px"
|
py="1px"
|
||||||
>
|
>
|
||||||
<Flex flexDir="column" gap={4} w="full" bg="base.850" p={3} borderRadius="base">
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
<Flex gap={3} alignItems="center" cursor="pointer">
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
<LayerVisibilityToggle layerId={layerId} />
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
<LayerTitle type="control_adapter_layer" />
|
<LayerTitle type="control_adapter_layer" />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
@ -56,8 +58,12 @@ export const CALayerListItem = memo(({ layerId }: Props) => {
|
|||||||
<LayerMenu layerId={layerId} />
|
<LayerMenu layerId={layerId} />
|
||||||
<LayerDeleteButton layerId={layerId} />
|
<LayerDeleteButton layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{isOpen && (
|
||||||
|
<Flex gap={3} px={3} pb={3}>
|
||||||
<ControlAdapterLayerConfig id={controlNetId} />
|
<ControlAdapterLayerConfig id={controlNetId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
import { useLayerOpacity } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
import { isFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
|
import { isFilterEnabledChanged, layerOpacityChanged } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
@ -51,6 +52,7 @@ const CALayerOpacity = ({ layerId }: Props) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
icon={<PiDropHalfFill size={16} />}
|
icon={<PiDropHalfFill size={16} />}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
onDoubleClick={stopPropagation}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig';
|
import ControlAdapterLayerConfig from 'features/controlLayers/components/controlAdapterOverrides/ControlAdapterLayerConfig';
|
||||||
@ -24,17 +24,22 @@ export const IPLayerListItem = memo(({ layerId }: Props) => {
|
|||||||
[layerId]
|
[layerId]
|
||||||
);
|
);
|
||||||
const ipAdapterId = useAppSelector(selector);
|
const ipAdapterId = useAppSelector(selector);
|
||||||
|
const { isOpen, onToggle } = useDisclosure();
|
||||||
return (
|
return (
|
||||||
<Flex gap={2} bg="base.800" borderRadius="base" p="1px">
|
<Flex gap={2} bg="base.800" borderRadius="base" p="1px" px={2}>
|
||||||
<Flex flexDir="column" gap={4} w="full" bg="base.850" p={3} borderRadius="base">
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
<Flex gap={3} alignItems="center">
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
<LayerVisibilityToggle layerId={layerId} />
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
<LayerTitle type="ip_adapter_layer" />
|
<LayerTitle type="ip_adapter_layer" />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<LayerDeleteButton layerId={layerId} />
|
<LayerDeleteButton layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{isOpen && (
|
||||||
|
<Flex gap={3} alignItems="center" px={3} pb={3} cursor="pointer">
|
||||||
<ControlAdapterLayerConfig id={ipAdapterId} />
|
<ControlAdapterLayerConfig id={ipAdapterId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
import { guidanceLayerDeleted } from 'app/store/middleware/listenerMiddleware/listeners/controlLayersToControlAdapterBridge';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||||
@ -21,6 +22,7 @@ export const LayerDeleteButton = memo(({ layerId }: Props) => {
|
|||||||
tooltip={t('common.delete')}
|
tooltip={t('common.delete')}
|
||||||
icon={<PiTrashSimpleBold />}
|
icon={<PiTrashSimpleBold />}
|
||||||
onClick={deleteLayer}
|
onClick={deleteLayer}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
import { IconButton, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerMenuArrangeActions';
|
import { LayerMenuArrangeActions } from 'features/controlLayers/components/LayerMenuArrangeActions';
|
||||||
import { LayerMenuRGActions } from 'features/controlLayers/components/LayerMenuRGActions';
|
import { LayerMenuRGActions } from 'features/controlLayers/components/LayerMenuRGActions';
|
||||||
import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks';
|
import { useLayerType } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
@ -22,7 +23,13 @@ export const LayerMenu = memo(({ layerId }: Props) => {
|
|||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton as={IconButton} aria-label="Layer menu" size="sm" icon={<PiDotsThreeVerticalBold />} />
|
<MenuButton
|
||||||
|
as={IconButton}
|
||||||
|
aria-label="Layer menu"
|
||||||
|
size="sm"
|
||||||
|
icon={<PiDotsThreeVerticalBold />}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
{layerType === 'regional_guidance_layer' && (
|
{layerType === 'regional_guidance_layer' && (
|
||||||
<>
|
<>
|
||||||
|
@ -20,7 +20,7 @@ export const LayerTitle = memo(({ type }: Props) => {
|
|||||||
}, [t, type]);
|
}, [t, type]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text size="sm" fontWeight="semibold" pointerEvents="none" color="base.300">
|
<Text size="sm" fontWeight="semibold" userSelect="none" color="base.300">
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { IconButton } from '@invoke-ai/ui-library';
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { useLayerIsVisible } from 'features/controlLayers/hooks/layerStateHooks';
|
import { useLayerIsVisible } from 'features/controlLayers/hooks/layerStateHooks';
|
||||||
import { layerVisibilityToggled } from 'features/controlLayers/store/controlLayersSlice';
|
import { layerVisibilityToggled } from 'features/controlLayers/store/controlLayersSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
@ -27,6 +28,7 @@ export const LayerVisibilityToggle = memo(({ layerId }: Props) => {
|
|||||||
icon={isVisible ? <PiCheckBold /> : undefined}
|
icon={isVisible ? <PiCheckBold /> : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
colorScheme="base"
|
colorScheme="base"
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger, Tooltip } f
|
|||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import RgbColorPicker from 'common/components/RgbColorPicker';
|
import RgbColorPicker from 'common/components/RgbColorPicker';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
import {
|
import {
|
||||||
isRegionalGuidanceLayer,
|
isRegionalGuidanceLayer,
|
||||||
@ -51,6 +52,7 @@ export const RGLayerColorPicker = memo(({ layerId }: Props) => {
|
|||||||
h={8}
|
h={8}
|
||||||
cursor="pointer"
|
cursor="pointer"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Badge, Flex, Spacer } from '@invoke-ai/ui-library';
|
import { Badge, Flex, Spacer, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
import { rgbColorToString } from 'features/canvas/util/colorToString';
|
||||||
@ -47,26 +47,19 @@ export const RGLayerListItem = memo(({ layerId }: Props) => {
|
|||||||
);
|
);
|
||||||
const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } =
|
const { autoNegative, color, hasPositivePrompt, hasNegativePrompt, hasIPAdapters, isSelected } =
|
||||||
useAppSelector(selector);
|
useAppSelector(selector);
|
||||||
const onClickCapture = useCallback(() => {
|
const { isOpen, onToggle } = useDisclosure();
|
||||||
// Must be capture so that the layer is selected before deleting/resetting/etc
|
const onClick = useCallback(() => {
|
||||||
dispatch(layerSelected(layerId));
|
dispatch(layerSelected(layerId));
|
||||||
}, [dispatch, layerId]);
|
}, [dispatch, layerId]);
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex gap={2} onClick={onClick} bg={isSelected ? color : 'base.800'} px={2} borderRadius="base" py="1px">
|
||||||
gap={2}
|
<Flex flexDir="column" w="full" bg="base.850" borderRadius="base">
|
||||||
onClickCapture={onClickCapture}
|
<Flex gap={3} alignItems="center" p={3} cursor="pointer" onDoubleClick={onToggle}>
|
||||||
bg={isSelected ? color : 'base.800'}
|
|
||||||
px={2}
|
|
||||||
borderRadius="base"
|
|
||||||
py="1px"
|
|
||||||
>
|
|
||||||
<Flex flexDir="column" w="full" bg="base.850" p={3} gap={3} borderRadius="base">
|
|
||||||
<Flex gap={3} alignItems="center" cursor="pointer">
|
|
||||||
<LayerVisibilityToggle layerId={layerId} />
|
<LayerVisibilityToggle layerId={layerId} />
|
||||||
<LayerTitle type="regional_guidance_layer" />
|
<LayerTitle type="regional_guidance_layer" />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
{autoNegative === 'invert' && (
|
{autoNegative === 'invert' && (
|
||||||
<Badge color="base.300" bg="transparent" borderWidth={1}>
|
<Badge color="base.300" bg="transparent" borderWidth={1} userSelect="none">
|
||||||
{t('controlLayers.autoNegative')}
|
{t('controlLayers.autoNegative')}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
@ -75,11 +68,15 @@ export const RGLayerListItem = memo(({ layerId }: Props) => {
|
|||||||
<LayerMenu layerId={layerId} />
|
<LayerMenu layerId={layerId} />
|
||||||
<LayerDeleteButton layerId={layerId} />
|
<LayerDeleteButton layerId={layerId} />
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{isOpen && (
|
||||||
|
<Flex gap={3} px={3} pb={3}>
|
||||||
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
|
{!hasPositivePrompt && !hasNegativePrompt && !hasIPAdapters && <AddPromptButtons layerId={layerId} />}
|
||||||
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
|
{hasPositivePrompt && <RGLayerPositivePrompt layerId={layerId} />}
|
||||||
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
|
{hasNegativePrompt && <RGLayerNegativePrompt layerId={layerId} />}
|
||||||
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
|
{hasIPAdapters && <RGLayerIPAdapterList layerId={layerId} />}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@invoke-ai/ui-library';
|
} from '@invoke-ai/ui-library';
|
||||||
|
import { stopPropagation } from 'common/util/stopPropagation';
|
||||||
import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayerAutoNegativeCheckbox';
|
import { RGLayerAutoNegativeCheckbox } from 'features/controlLayers/components/RGLayerAutoNegativeCheckbox';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@ -34,6 +35,7 @@ const RGLayerSettingsPopover = ({ layerId }: Props) => {
|
|||||||
aria-label={t('common.settingsLabel')}
|
aria-label={t('common.settingsLabel')}
|
||||||
size="sm"
|
size="sm"
|
||||||
icon={<PiGearSixBold />}
|
icon={<PiGearSixBold />}
|
||||||
|
onDoubleClick={stopPropagation} // double click expands the layer
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent>
|
<PopoverContent>
|
||||||
|
@ -22,7 +22,7 @@ const ControlAdapterLayerConfig = (props: { id: string }) => {
|
|||||||
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
const [isExpanded, toggleIsExpanded] = useToggle(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" gap={4} position="relative">
|
<Flex flexDir="column" gap={4} position="relative" w="full">
|
||||||
<Flex gap={3} alignItems="center" w="full">
|
<Flex gap={3} alignItems="center" w="full">
|
||||||
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
<Box minW={0} w="full" transitionProperty="common" transitionDuration="0.1s">
|
||||||
<ParamControlAdapterModel id={id} />{' '}
|
<ParamControlAdapterModel id={id} />{' '}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user