mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): iterate on layer actions
- Add lock toggle - Tweak lock and enabled styles - Update entity list action bar w/ delete & delete all - Move add layer menu to action bar - Adjust opacity slider style
This commit is contained in:
parent
77f020a997
commit
377db3f726
@ -1658,7 +1658,6 @@
|
|||||||
"autoSave": "Auto-save to Gallery",
|
"autoSave": "Auto-save to Gallery",
|
||||||
"resetCanvas": "Reset Canvas",
|
"resetCanvas": "Reset Canvas",
|
||||||
"resetAll": "Reset All",
|
"resetAll": "Reset All",
|
||||||
"deleteAll": "Delete All",
|
|
||||||
"clearCaches": "Clear Caches",
|
"clearCaches": "Clear Caches",
|
||||||
"recalculateRects": "Recalculate Rects",
|
"recalculateRects": "Recalculate Rects",
|
||||||
"clipToBbox": "Clip Strokes to Bbox",
|
"clipToBbox": "Clip Strokes to Bbox",
|
||||||
@ -1735,6 +1734,10 @@
|
|||||||
"showingType": "Showing {{type}}",
|
"showingType": "Showing {{type}}",
|
||||||
"dynamicGrid": "Dynamic Grid",
|
"dynamicGrid": "Dynamic Grid",
|
||||||
"logDebugInfo": "Log Debug Info",
|
"logDebugInfo": "Log Debug Info",
|
||||||
|
"locked": "Locked",
|
||||||
|
"unlocked": "Unlocked",
|
||||||
|
"deleteSelected": "Delete Selected",
|
||||||
|
"deleteAll": "Delete All",
|
||||||
"fill": {
|
"fill": {
|
||||||
"fillStyle": "Fill Style",
|
"fillStyle": "Fill Style",
|
||||||
"solid": "Solid",
|
"solid": "Solid",
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent';
|
||||||
import { CanvasEntityOpacity } from 'features/controlLayers/components/common/CanvasEntityOpacity';
|
|
||||||
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
|
import { ControlLayerEntityList } from 'features/controlLayers/components/ControlLayer/ControlLayerEntityList';
|
||||||
import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList';
|
import { InpaintMaskList } from 'features/controlLayers/components/InpaintMask/InpaintMaskList';
|
||||||
import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
|
import { IPAdapterList } from 'features/controlLayers/components/IPAdapter/IPAdapterList';
|
||||||
@ -11,8 +10,7 @@ import { memo } from 'react';
|
|||||||
export const CanvasEntityList = memo(() => {
|
export const CanvasEntityList = memo(() => {
|
||||||
return (
|
return (
|
||||||
<ScrollableContent>
|
<ScrollableContent>
|
||||||
<Flex flexDir="column" gap={2} pt={2} data-testid="control-layers-layer-list" w="full" h="full">
|
<Flex flexDir="column" gap={2} data-testid="control-layers-layer-list" w="full" h="full">
|
||||||
<CanvasEntityOpacity />
|
|
||||||
<InpaintMaskList />
|
<InpaintMaskList />
|
||||||
<RegionalGuidanceEntityList />
|
<RegionalGuidanceEntityList />
|
||||||
<IPAdapterList />
|
<IPAdapterList />
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import { EntityListActionBarAddLayerButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuButton';
|
||||||
|
import { EntityListActionBarDeleteButton } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarDeleteButton';
|
||||||
|
import { SelectedEntityOpacity } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarSelectedEntityOpacity';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const EntityListActionBar = memo(() => {
|
||||||
|
return (
|
||||||
|
<Flex w="full" py={1} px={1} gap={2}>
|
||||||
|
<SelectedEntityOpacity />
|
||||||
|
<Spacer />
|
||||||
|
<EntityListActionBarAddLayerButton />
|
||||||
|
<EntityListActionBarDeleteButton />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EntityListActionBar.displayName = 'EntityListActionBar';
|
@ -1,21 +1,22 @@
|
|||||||
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
import { IconButton, Menu, MenuButton, MenuList } from '@invoke-ai/ui-library';
|
||||||
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems';
|
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBarAddLayerMenuItems';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiDotsThreeOutlineFill } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const CanvasEntityListMenuButton = memo(() => {
|
export const EntityListActionBarAddLayerButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={IconButton}
|
as={IconButton}
|
||||||
aria-label={t('accessibility.menu')}
|
size="sm"
|
||||||
icon={<PiDotsThreeOutlineFill />}
|
tooltip={t('controlLayers.addLayer')}
|
||||||
variant="link"
|
aria-label={t('controlLayers.addLayer')}
|
||||||
|
icon={<PiPlusBold />}
|
||||||
|
variant="ghost"
|
||||||
data-testid="control-layers-add-layer-menu-button"
|
data-testid="control-layers-add-layer-menu-button"
|
||||||
alignSelf="stretch"
|
|
||||||
/>
|
/>
|
||||||
<MenuList>
|
<MenuList>
|
||||||
<CanvasEntityListMenuItems />
|
<CanvasEntityListMenuItems />
|
||||||
@ -24,4 +25,4 @@ export const CanvasEntityListMenuButton = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CanvasEntityListMenuButton.displayName = 'CanvasEntityListMenuButton';
|
EntityListActionBarAddLayerButton.displayName = 'EntityListActionBarAddLayerButton';
|
@ -1,22 +1,19 @@
|
|||||||
import { MenuDivider, MenuItem } from '@invoke-ai/ui-library';
|
import { MenuItem } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
import {
|
import {
|
||||||
allEntitiesDeleted,
|
|
||||||
controlLayerAdded,
|
controlLayerAdded,
|
||||||
inpaintMaskAdded,
|
inpaintMaskAdded,
|
||||||
ipaAdded,
|
ipaAdded,
|
||||||
rasterLayerAdded,
|
rasterLayerAdded,
|
||||||
rgAdded,
|
rgAdded,
|
||||||
} from 'features/controlLayers/store/canvasSlice';
|
} from 'features/controlLayers/store/canvasSlice';
|
||||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi';
|
import { PiPlusBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const CanvasEntityListMenuItems = memo(() => {
|
export const CanvasEntityListMenuItems = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const hasEntities = useAppSelector(selectHasEntities);
|
|
||||||
const addInpaintMask = useCallback(() => {
|
const addInpaintMask = useCallback(() => {
|
||||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
@ -32,9 +29,6 @@ export const CanvasEntityListMenuItems = memo(() => {
|
|||||||
const addIPAdapter = useCallback(() => {
|
const addIPAdapter = useCallback(() => {
|
||||||
dispatch(ipaAdded({ isSelected: true }));
|
dispatch(ipaAdded({ isSelected: true }));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
const deleteAll = useCallback(() => {
|
|
||||||
dispatch(allEntitiesDeleted());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -53,10 +47,6 @@ export const CanvasEntityListMenuItems = memo(() => {
|
|||||||
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
<MenuItem icon={<PiPlusBold />} onClick={addIPAdapter}>
|
||||||
{t('controlLayers.ipAdapter', { count: 1 })}
|
{t('controlLayers.ipAdapter', { count: 1 })}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuDivider />
|
|
||||||
<MenuItem onClick={deleteAll} icon={<PiTrashSimpleBold />} color="error.300" isDisabled={!hasEntities}>
|
|
||||||
{t('controlLayers.deleteAll', { count: 1 })}
|
|
||||||
</MenuItem>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
@ -0,0 +1,39 @@
|
|||||||
|
import { IconButton, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { allEntitiesDeleted, entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||||
|
import { selectEntityCount, selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||||
|
import { memo, useCallback } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiTrashSimpleFill } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const EntityListActionBarDeleteButton = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||||
|
const entityCount = useAppSelector(selectEntityCount);
|
||||||
|
const shift = useShiftModifier();
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
if (shift) {
|
||||||
|
dispatch(allEntitiesDeleted());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!selectedEntityIdentifier) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dispatch(entityDeleted({ entityIdentifier: selectedEntityIdentifier }));
|
||||||
|
}, [dispatch, selectedEntityIdentifier, shift]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
onClick={onClick}
|
||||||
|
isDisabled={shift ? entityCount === 0 : !selectedEntityIdentifier}
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
aria-label={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
|
||||||
|
tooltip={shift ? t('controlLayers.deleteAll') : t('controlLayers.deleteSelected')}
|
||||||
|
icon={<PiTrashSimpleFill />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
EntityListActionBarDeleteButton.displayName = 'EntityListActionBarDeleteButton';
|
@ -77,7 +77,7 @@ const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
|
|||||||
return selectedEntity.opacity;
|
return selectedEntity.opacity;
|
||||||
});
|
});
|
||||||
|
|
||||||
export const CanvasEntityOpacity = memo(() => {
|
export const SelectedEntityOpacity = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||||
@ -151,6 +151,8 @@ export const CanvasEntityOpacity = memo(() => {
|
|||||||
defaultValue={1}
|
defaultValue={1}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
clampValueOnBlur={false}
|
clampValueOnBlur={false}
|
||||||
|
variant="outline"
|
||||||
|
isDisabled={selectedEntityIdentifier === null || selectedEntityIdentifier.type === 'ip_adapter'}
|
||||||
>
|
>
|
||||||
<NumberInputField paddingInlineEnd={7} />
|
<NumberInputField paddingInlineEnd={7} />
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
@ -186,4 +188,4 @@ export const CanvasEntityOpacity = memo(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
CanvasEntityOpacity.displayName = 'CanvasEntityOpacity';
|
SelectedEntityOpacity.displayName = 'SelectedEntityOpacity';
|
@ -1,32 +1,22 @@
|
|||||||
import { Box, ContextMenu, MenuList } from '@invoke-ai/ui-library';
|
import { Divider, Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
import { CanvasAddEntityButtons } from 'features/controlLayers/components/CanvasAddEntityButtons';
|
||||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||||
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems';
|
import { EntityListActionBar } from 'features/controlLayers/components/CanvasEntityList/EntityListActionBar';
|
||||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
export const CanvasPanelContent = memo(() => {
|
export const CanvasPanelContent = memo(() => {
|
||||||
const hasEntities = useAppSelector(selectHasEntities);
|
const hasEntities = useAppSelector(selectHasEntities);
|
||||||
const renderMenu = useCallback(
|
|
||||||
() => (
|
|
||||||
<MenuList>
|
|
||||||
<CanvasEntityListMenuItems />
|
|
||||||
</MenuList>
|
|
||||||
),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<CanvasManagerProviderGate>
|
<CanvasManagerProviderGate>
|
||||||
<ContextMenu<HTMLDivElement> renderMenu={renderMenu}>
|
<Flex flexDir="column" gap={2} w="full" h="full">
|
||||||
{(ref) => (
|
<EntityListActionBar />
|
||||||
<Box ref={ref} w="full" h="full">
|
<Divider py={0} />
|
||||||
{!hasEntities && <CanvasAddEntityButtons />}
|
{!hasEntities && <CanvasAddEntityButtons />}
|
||||||
{hasEntities && <CanvasEntityList />}
|
{hasEntities && <CanvasEntityList />}
|
||||||
</Box>
|
</Flex>
|
||||||
)}
|
|
||||||
</ContextMenu>
|
|
||||||
</CanvasManagerProviderGate>
|
</CanvasManagerProviderGate>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library';
|
|||||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||||
|
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||||
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
|
import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
@ -28,6 +29,7 @@ export const ControlLayer = memo(({ id }: Props) => {
|
|||||||
<CanvasEntityEditableTitle />
|
<CanvasEntityEditableTitle />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ControlLayerBadges />
|
<ControlLayerBadges />
|
||||||
|
<CanvasEntityIsLockedToggle />
|
||||||
<CanvasEntityEnabledToggle />
|
<CanvasEntityEnabledToggle />
|
||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
<CanvasEntitySettingsWrapper>
|
<CanvasEntitySettingsWrapper>
|
||||||
|
@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library';
|
|||||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||||
|
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { EntityMaskAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
@ -26,6 +27,7 @@ export const InpaintMask = memo(({ id }: Props) => {
|
|||||||
<CanvasEntityPreviewImage />
|
<CanvasEntityPreviewImage />
|
||||||
<CanvasEntityEditableTitle />
|
<CanvasEntityEditableTitle />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<CanvasEntityIsLockedToggle />
|
||||||
<InpaintMaskMaskFillColorPicker />
|
<InpaintMaskMaskFillColorPicker />
|
||||||
<CanvasEntityEnabledToggle />
|
<CanvasEntityEnabledToggle />
|
||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
|
@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library';
|
|||||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||||
|
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
import { EntityLayerAdapterGate } from 'features/controlLayers/contexts/EntityAdapterContext';
|
||||||
@ -24,6 +25,7 @@ export const RasterLayer = memo(({ id }: Props) => {
|
|||||||
<CanvasEntityPreviewImage />
|
<CanvasEntityPreviewImage />
|
||||||
<CanvasEntityEditableTitle />
|
<CanvasEntityEditableTitle />
|
||||||
<Spacer />
|
<Spacer />
|
||||||
|
<CanvasEntityIsLockedToggle />
|
||||||
<CanvasEntityEnabledToggle />
|
<CanvasEntityEnabledToggle />
|
||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
</CanvasEntityContainer>
|
</CanvasEntityContainer>
|
||||||
|
@ -2,6 +2,7 @@ import { Spacer } from '@invoke-ai/ui-library';
|
|||||||
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
|
||||||
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
import { CanvasEntityEnabledToggle } from 'features/controlLayers/components/common/CanvasEntityEnabledToggle';
|
||||||
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
import { CanvasEntityHeader } from 'features/controlLayers/components/common/CanvasEntityHeader';
|
||||||
|
import { CanvasEntityIsLockedToggle } from 'features/controlLayers/components/common/CanvasEntityIsLockedToggle';
|
||||||
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
import { CanvasEntityPreviewImage } from 'features/controlLayers/components/common/CanvasEntityPreviewImage';
|
||||||
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
import { CanvasEntityEditableTitle } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
|
||||||
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
|
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
|
||||||
@ -30,6 +31,7 @@ export const RegionalGuidance = memo(({ id }: Props) => {
|
|||||||
<Spacer />
|
<Spacer />
|
||||||
<RegionalGuidanceBadges />
|
<RegionalGuidanceBadges />
|
||||||
<RegionalGuidanceMaskFillColorPicker />
|
<RegionalGuidanceMaskFillColorPicker />
|
||||||
|
<CanvasEntityIsLockedToggle />
|
||||||
<CanvasEntityEnabledToggle />
|
<CanvasEntityEnabledToggle />
|
||||||
</CanvasEntityHeader>
|
</CanvasEntityHeader>
|
||||||
<RegionalGuidanceSettings />
|
<RegionalGuidanceSettings />
|
||||||
|
@ -11,7 +11,7 @@ const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
|||||||
return canvas.regions.entities.map(mapId).reverse();
|
return canvas.regions.entities.map(mapId).reverse();
|
||||||
});
|
});
|
||||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||||
return selectedEntityIdentifier?.type === 'raster_layer';
|
return selectedEntityIdentifier?.type === 'regional_guidance';
|
||||||
});
|
});
|
||||||
|
|
||||||
export const RegionalGuidanceEntityList = memo(() => {
|
export const RegionalGuidanceEntityList = memo(() => {
|
||||||
|
@ -1,32 +1,35 @@
|
|||||||
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 { useBoolean } from 'common/hooks/useBoolean';
|
||||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
import { useEntityIsEnabled } from 'features/controlLayers/hooks/useEntityIsEnabled';
|
||||||
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
|
import { entityIsEnabledToggled } from 'features/controlLayers/store/canvasSlice';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { PiCheckBold } from 'react-icons/pi';
|
import { PiCheckBold } from 'react-icons/pi';
|
||||||
|
|
||||||
export const CanvasEntityEnabledToggle = memo(() => {
|
export const CanvasEntityEnabledToggle = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const entityIdentifier = useEntityIdentifierContext();
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
const isEnabled = useEntityIsEnabled(entityIdentifier);
|
const isEnabled = useEntityIsEnabled(entityIdentifier);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(entityIsEnabledToggled({ entityIdentifier }));
|
dispatch(entityIsEnabledToggled({ entityIdentifier }));
|
||||||
}, [dispatch, entityIdentifier]);
|
}, [dispatch, entityIdentifier]);
|
||||||
|
const isHovered = useBoolean(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
ref={ref}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
onMouseOver={isHovered.setTrue}
|
||||||
|
onMouseOut={isHovered.setFalse}
|
||||||
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
aria-label={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||||
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
tooltip={t(isEnabled ? 'common.enabled' : 'common.disabled')}
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
icon={isEnabled ? <PiCheckBold /> : undefined}
|
icon={isEnabled || isHovered.isTrue ? <PiCheckBold /> : undefined}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onDoubleClick={stopPropagation} // double click expands the layer
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -38,13 +38,13 @@ export const CanvasEntityGroupList = memo(({ isSelected, type, children }: Props
|
|||||||
boxSize={4}
|
boxSize={4}
|
||||||
as={PiCaretDownBold}
|
as={PiCaretDownBold}
|
||||||
transform={collapse.isTrue ? undefined : 'rotate(-90deg)'}
|
transform={collapse.isTrue ? undefined : 'rotate(-90deg)'}
|
||||||
fill={isSelected ? 'invokeBlue.300' : 'base.300'}
|
fill={isSelected ? 'base.200' : 'base.500'}
|
||||||
transitionProperty="common"
|
transitionProperty="common"
|
||||||
transitionDuration="fast"
|
transitionDuration="fast"
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
fontWeight="semibold"
|
fontWeight="semibold"
|
||||||
color={isSelected ? 'invokeBlue.300' : 'base.300'}
|
color={isSelected ? 'base.200' : 'base.500'}
|
||||||
userSelect="none"
|
userSelect="none"
|
||||||
transitionProperty="common"
|
transitionProperty="common"
|
||||||
transitionDuration="fast"
|
transitionDuration="fast"
|
||||||
|
@ -0,0 +1,37 @@
|
|||||||
|
import { IconButton } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { useBoolean } from 'common/hooks/useBoolean';
|
||||||
|
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||||
|
import { useEntityIsLocked } from 'features/controlLayers/hooks/useEntityIsLocked';
|
||||||
|
import { entityIsLockedToggled } from 'features/controlLayers/store/canvasSlice';
|
||||||
|
import { memo, useCallback, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiLockSimpleFill } from 'react-icons/pi';
|
||||||
|
|
||||||
|
export const CanvasEntityIsLockedToggle = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const entityIdentifier = useEntityIdentifierContext();
|
||||||
|
const ref = useRef<HTMLButtonElement>(null);
|
||||||
|
const isLocked = useEntityIsLocked(entityIdentifier);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(entityIsLockedToggled({ entityIdentifier }));
|
||||||
|
}, [dispatch, entityIdentifier]);
|
||||||
|
const isHovered = useBoolean(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
ref={ref}
|
||||||
|
size="sm"
|
||||||
|
onMouseOver={isHovered.setTrue}
|
||||||
|
onMouseOut={isHovered.setFalse}
|
||||||
|
aria-label={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||||
|
tooltip={t(isLocked ? 'controlLayers.locked' : 'controlLayers.unlocked')}
|
||||||
|
variant="ghost"
|
||||||
|
icon={isLocked || isHovered.isTrue ? <PiLockSimpleFill /> : undefined}
|
||||||
|
onClick={onClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
CanvasEntityIsLockedToggle.displayName = 'CanvasEntityIsLockedToggle';
|
@ -0,0 +1,22 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||||
|
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
export const useEntityIsLocked = (entityIdentifier: CanvasEntityIdentifier) => {
|
||||||
|
const selectIsLocked = useMemo(
|
||||||
|
() =>
|
||||||
|
createSelector(selectCanvasSlice, (canvas) => {
|
||||||
|
const entity = selectEntity(canvas, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return entity.isLocked;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
[entityIdentifier]
|
||||||
|
);
|
||||||
|
const isLocked = useAppSelector(selectIsLocked);
|
||||||
|
return isLocked;
|
||||||
|
};
|
@ -131,6 +131,7 @@ export const canvasSlice = createSlice({
|
|||||||
name: null,
|
name: null,
|
||||||
type: 'raster_layer',
|
type: 'raster_layer',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isLocked: false,
|
||||||
objects: [],
|
objects: [],
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
@ -191,6 +192,7 @@ export const canvasSlice = createSlice({
|
|||||||
name: null,
|
name: null,
|
||||||
type: 'control_layer',
|
type: 'control_layer',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isLocked: false,
|
||||||
withTransparencyEffect: true,
|
withTransparencyEffect: true,
|
||||||
objects: [],
|
objects: [],
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
@ -332,6 +334,7 @@ export const canvasSlice = createSlice({
|
|||||||
id,
|
id,
|
||||||
type: 'ip_adapter',
|
type: 'ip_adapter',
|
||||||
name: null,
|
name: null,
|
||||||
|
isLocked: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
ipAdapter: deepClone(initialIPAdapter),
|
ipAdapter: deepClone(initialIPAdapter),
|
||||||
};
|
};
|
||||||
@ -420,6 +423,7 @@ export const canvasSlice = createSlice({
|
|||||||
const entity: CanvasRegionalGuidanceState = {
|
const entity: CanvasRegionalGuidanceState = {
|
||||||
id,
|
id,
|
||||||
name: null,
|
name: null,
|
||||||
|
isLocked: false,
|
||||||
type: 'regional_guidance',
|
type: 'regional_guidance',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
objects: [],
|
objects: [],
|
||||||
@ -630,6 +634,7 @@ export const canvasSlice = createSlice({
|
|||||||
name: null,
|
name: null,
|
||||||
type: 'inpaint_mask',
|
type: 'inpaint_mask',
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isLocked: false,
|
||||||
objects: [],
|
objects: [],
|
||||||
opacity: 1,
|
opacity: 1,
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
@ -849,6 +854,14 @@ export const canvasSlice = createSlice({
|
|||||||
}
|
}
|
||||||
entity.isEnabled = !entity.isEnabled;
|
entity.isEnabled = !entity.isEnabled;
|
||||||
},
|
},
|
||||||
|
entityIsLockedToggled: (state, action: PayloadAction<EntityIdentifierPayload>) => {
|
||||||
|
const { entityIdentifier } = action.payload;
|
||||||
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
|
if (!entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
entity.isLocked = !entity.isLocked;
|
||||||
|
},
|
||||||
entityMoved: (state, action: PayloadAction<EntityMovedPayload>) => {
|
entityMoved: (state, action: PayloadAction<EntityMovedPayload>) => {
|
||||||
const { entityIdentifier, position } = action.payload;
|
const { entityIdentifier, position } = action.payload;
|
||||||
const entity = selectEntity(state, entityIdentifier);
|
const entity = selectEntity(state, entityIdentifier);
|
||||||
@ -1074,6 +1087,7 @@ export const {
|
|||||||
entityNameChanged,
|
entityNameChanged,
|
||||||
entityReset,
|
entityReset,
|
||||||
entityIsEnabledToggled,
|
entityIsEnabledToggled,
|
||||||
|
entityIsLockedToggled,
|
||||||
entityMoved,
|
entityMoved,
|
||||||
entityDuplicated,
|
entityDuplicated,
|
||||||
entityRasterized,
|
entityRasterized,
|
||||||
|
@ -529,11 +529,15 @@ const zIPAdapterConfig = z.object({
|
|||||||
});
|
});
|
||||||
export type IPAdapterConfig = z.infer<typeof zIPAdapterConfig>;
|
export type IPAdapterConfig = z.infer<typeof zIPAdapterConfig>;
|
||||||
|
|
||||||
const zCanvasIPAdapterState = z.object({
|
const zCanvasEntityBase = z.object({
|
||||||
id: zId,
|
id: zId,
|
||||||
name: zName,
|
name: zName,
|
||||||
type: z.literal('ip_adapter'),
|
|
||||||
isEnabled: z.boolean(),
|
isEnabled: z.boolean(),
|
||||||
|
isLocked: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const zCanvasIPAdapterState = zCanvasEntityBase.extend({
|
||||||
|
type: z.literal('ip_adapter'),
|
||||||
ipAdapter: zIPAdapterConfig,
|
ipAdapter: zIPAdapterConfig,
|
||||||
});
|
});
|
||||||
export type CanvasIPAdapterState = z.infer<typeof zCanvasIPAdapterState>;
|
export type CanvasIPAdapterState = z.infer<typeof zCanvasIPAdapterState>;
|
||||||
@ -555,11 +559,8 @@ const zRegionalGuidanceIPAdapterConfig = z.object({
|
|||||||
});
|
});
|
||||||
export type RegionalGuidanceIPAdapterConfig = z.infer<typeof zRegionalGuidanceIPAdapterConfig>;
|
export type RegionalGuidanceIPAdapterConfig = z.infer<typeof zRegionalGuidanceIPAdapterConfig>;
|
||||||
|
|
||||||
const zCanvasRegionalGuidanceState = z.object({
|
const zCanvasRegionalGuidanceState = zCanvasEntityBase.extend({
|
||||||
id: zId,
|
|
||||||
name: zName,
|
|
||||||
type: z.literal('regional_guidance'),
|
type: z.literal('regional_guidance'),
|
||||||
isEnabled: z.boolean(),
|
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
opacity: zOpacity,
|
opacity: zOpacity,
|
||||||
objects: z.array(zCanvasObjectState),
|
objects: z.array(zCanvasObjectState),
|
||||||
@ -571,11 +572,8 @@ const zCanvasRegionalGuidanceState = z.object({
|
|||||||
});
|
});
|
||||||
export type CanvasRegionalGuidanceState = z.infer<typeof zCanvasRegionalGuidanceState>;
|
export type CanvasRegionalGuidanceState = z.infer<typeof zCanvasRegionalGuidanceState>;
|
||||||
|
|
||||||
const zCanvasInpaintMaskState = z.object({
|
const zCanvasInpaintMaskState = zCanvasEntityBase.extend({
|
||||||
id: zId,
|
|
||||||
name: zName,
|
|
||||||
type: z.literal('inpaint_mask'),
|
type: z.literal('inpaint_mask'),
|
||||||
isEnabled: z.boolean(),
|
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
fill: zFill,
|
fill: zFill,
|
||||||
opacity: zOpacity,
|
opacity: zOpacity,
|
||||||
@ -600,11 +598,8 @@ const zT2IAdapterConfig = z.object({
|
|||||||
});
|
});
|
||||||
export type T2IAdapterConfig = z.infer<typeof zT2IAdapterConfig>;
|
export type T2IAdapterConfig = z.infer<typeof zT2IAdapterConfig>;
|
||||||
|
|
||||||
export const zCanvasRasterLayerState = z.object({
|
export const zCanvasRasterLayerState = zCanvasEntityBase.extend({
|
||||||
id: zId,
|
|
||||||
name: zName,
|
|
||||||
type: z.literal('raster_layer'),
|
type: z.literal('raster_layer'),
|
||||||
isEnabled: z.boolean(),
|
|
||||||
position: zCoordinate,
|
position: zCoordinate,
|
||||||
opacity: zOpacity,
|
opacity: zOpacity,
|
||||||
objects: z.array(zCanvasObjectState),
|
objects: z.array(zCanvasObjectState),
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||||
import { Box, Flex, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
import { Box, Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||||
import { CanvasEntityListMenuButton } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton';
|
|
||||||
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
|
import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent';
|
||||||
import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice';
|
||||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||||
@ -100,8 +99,6 @@ const ParametersPanelTextToImage = () => {
|
|||||||
>
|
>
|
||||||
{controlLayersTitle}
|
{controlLayersTitle}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Spacer />
|
|
||||||
<CanvasEntityListMenuButton />
|
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels w="full" h="full">
|
<TabPanels w="full" h="full">
|
||||||
<TabPanel p={0} w="full" h="full">
|
<TabPanel p={0} w="full" h="full">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user