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:
psychedelicious 2024-08-27 19:08:57 +10:00
parent 77f020a997
commit 377db3f726
20 changed files with 190 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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