feat(ui): collapsible layers

This commit is contained in:
psychedelicious 2024-04-30 19:45:09 +10:00 committed by Kent Keirsey
parent cf1883585d
commit d74cd12aa6
12 changed files with 58 additions and 30 deletions

View File

@ -0,0 +1,3 @@
export const stopPropagation = (e: React.MouseEvent) => {
e.stopPropagation();
};

View File

@ -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>
); );
}); });

View File

@ -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>

View File

@ -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>
); );
}); });

View File

@ -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
/> />
); );
}); });

View File

@ -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' && (
<> <>

View File

@ -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>
); );

View File

@ -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
/> />
); );
}); });

View File

@ -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>

View File

@ -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>
); );
}); });

View File

@ -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>

View File

@ -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} />{' '}