feat(ui): rename layers

This commit is contained in:
psychedelicious 2024-08-15 11:58:44 +10:00
parent 72919fa34e
commit 5f061ac1e2
10 changed files with 112 additions and 11 deletions

View File

@ -1,10 +1,11 @@
import { Spacer } from '@invoke-ai/ui-library'; import { Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper'; import { CanvasEntitySettingsWrapper } from 'features/controlLayers/components/common/CanvasEntitySettingsWrapper';
import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter'; import { ControlLayerControlAdapter } from 'features/controlLayers/components/ControlLayer/ControlLayerControlAdapter';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
@ -16,13 +17,14 @@ type Props = {
export const ControlLayer = memo(({ id }: Props) => { export const ControlLayer = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'control_layer' }), [id]); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'control_layer' }), [id]);
const editing = useDisclosure({ defaultIsOpen: false });
return ( return (
<EntityIdentifierContext.Provider value={entityIdentifier}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer> <CanvasEntityContainer>
<CanvasEntityHeader> <CanvasEntityHeader onDoubleClick={editing.onOpen}>
<CanvasEntityEnabledToggle /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle /> {editing.isOpen ? <CanvasEntityTitleEdit onStopEditing={editing.onClose} /> : <CanvasEntityTitle />}
<Spacer /> <Spacer />
<CanvasEntityDeleteButton /> <CanvasEntityDeleteButton />
</CanvasEntityHeader> </CanvasEntityHeader>

View File

@ -1,9 +1,10 @@
import { Spacer } from '@invoke-ai/ui-library'; import { Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
@ -14,13 +15,14 @@ type Props = {
export const RasterLayer = memo(({ id }: Props) => { export const RasterLayer = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'raster_layer' }), [id]); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'raster_layer' }), [id]);
const editing = useDisclosure({ defaultIsOpen: false });
return ( return (
<EntityIdentifierContext.Provider value={entityIdentifier}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer> <CanvasEntityContainer>
<CanvasEntityHeader> <CanvasEntityHeader onDoubleClick={editing.onOpen}>
<CanvasEntityEnabledToggle /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle /> {editing.isOpen ? <CanvasEntityTitleEdit onStopEditing={editing.onClose} /> : <CanvasEntityTitle />}
<Spacer /> <Spacer />
<CanvasEntityDeleteButton /> <CanvasEntityDeleteButton />
</CanvasEntityHeader> </CanvasEntityHeader>

View File

@ -1,9 +1,10 @@
import { Spacer } from '@invoke-ai/ui-library'; import { Spacer, useDisclosure } from '@invoke-ai/ui-library';
import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer'; import { CanvasEntityContainer } from 'features/controlLayers/components/common/CanvasEntityContainer';
import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton'; import { CanvasEntityDeleteButton } from 'features/controlLayers/components/common/CanvasEntityDeleteButton';
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 { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle'; import { CanvasEntityTitle } from 'features/controlLayers/components/common/CanvasEntityTitle';
import { CanvasEntityTitleEdit } from 'features/controlLayers/components/common/CanvasEntityTitleEdit';
import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges'; import { RegionalGuidanceBadges } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceBadges';
import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings'; import { RegionalGuidanceSettings } from 'features/controlLayers/components/RegionalGuidance/RegionalGuidanceSettings';
import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext'; import { EntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
@ -19,12 +20,14 @@ type Props = {
export const RegionalGuidance = memo(({ id }: Props) => { export const RegionalGuidance = memo(({ id }: Props) => {
const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'regional_guidance' }), [id]); const entityIdentifier = useMemo<CanvasEntityIdentifier>(() => ({ id, type: 'regional_guidance' }), [id]);
const editing = useDisclosure({ defaultIsOpen: false });
return ( return (
<EntityIdentifierContext.Provider value={entityIdentifier}> <EntityIdentifierContext.Provider value={entityIdentifier}>
<CanvasEntityContainer> <CanvasEntityContainer>
<CanvasEntityHeader> <CanvasEntityHeader onDoubleClick={editing.onOpen}>
<CanvasEntityEnabledToggle /> <CanvasEntityEnabledToggle />
<CanvasEntityTitle /> {editing.isOpen ? <CanvasEntityTitleEdit onStopEditing={editing.onClose} /> : <CanvasEntityTitle />}
<Spacer /> <Spacer />
<RegionalGuidanceBadges /> <RegionalGuidanceBadges />
<RegionalGuidanceMaskFillColorPicker /> <RegionalGuidanceMaskFillColorPicker />

View File

@ -0,0 +1,56 @@
import { Input } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle';
import { entityNameChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ChangeEvent, KeyboardEvent } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
type Props = {
onStopEditing: () => void;
};
export const CanvasEntityTitleEdit = memo(({ onStopEditing }: Props) => {
const dispatch = useAppDispatch();
const ref = useRef<HTMLInputElement>(null);
const entityIdentifier = useEntityIdentifierContext();
const title = useEntityTitle(entityIdentifier);
const [localTitle, setLocalTitle] = useState(title);
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setLocalTitle(e.target.value);
}, []);
const onBlur = useCallback(() => {
const trimmedTitle = localTitle.trim();
if (trimmedTitle.length === 0) {
dispatch(entityNameChanged({ entityIdentifier, name: null }));
} else if (trimmedTitle !== title) {
dispatch(entityNameChanged({ entityIdentifier, name: trimmedTitle }));
}
onStopEditing();
}, [dispatch, entityIdentifier, localTitle, onStopEditing, title]);
const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onBlur();
} else if (e.key === 'Escape') {
setLocalTitle(title);
onStopEditing();
}
},
[onBlur, onStopEditing, title]
);
useEffect(() => {
ref.current?.focus();
ref.current?.select();
}, []);
return (
<Input ref={ref} value={localTitle} onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown} variant="outline" />
);
});
CanvasEntityTitleEdit.displayName = 'CanvasEntityTitleEdit';

View File

@ -1,15 +1,35 @@
import { createSelector } from '@reduxjs/toolkit';
import { useAppSelector } from 'app/store/storeHooks';
import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount'; import { useEntityObjectCount } from 'features/controlLayers/hooks/useEntityObjectCount';
import { selectCanvasV2Slice, selectEntity } from 'features/controlLayers/store/canvasV2Slice';
import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types'; import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { assert } from 'tsafe'; import { assert } from 'tsafe';
const createSelectName = (entityIdentifier: CanvasEntityIdentifier) =>
createSelector(selectCanvasV2Slice, (canvasV2) => {
const entity = selectEntity(canvasV2, entityIdentifier);
if (!entity) {
return null;
}
if (entity.type === 'inpaint_mask') {
return null;
}
return entity.name;
});
export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => { export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
const { t } = useTranslation(); const { t } = useTranslation();
const selectName = useMemo(() => createSelectName(entityIdentifier), [entityIdentifier]);
const name = useAppSelector(selectName);
const objectCount = useEntityObjectCount(entityIdentifier); const objectCount = useEntityObjectCount(entityIdentifier);
const title = useMemo(() => { const title = useMemo(() => {
if (name) {
return name;
}
const parts: string[] = []; const parts: string[] = [];
if (entityIdentifier.type === 'inpaint_mask') { if (entityIdentifier.type === 'inpaint_mask') {
parts.push(t('controlLayers.inpaintMask')); parts.push(t('controlLayers.inpaintMask'));
@ -30,7 +50,7 @@ export const useEntityTitle = (entityIdentifier: CanvasEntityIdentifier) => {
} }
return parts.join(' '); return parts.join(' ');
}, [entityIdentifier.type, objectCount, t]); }, [entityIdentifier.type, name, objectCount, t]);
return title; return title;
}; };

View File

@ -198,6 +198,18 @@ export const canvasV2Slice = createSlice({
const { entityIdentifier } = action.payload; const { entityIdentifier } = action.payload;
state.selectedEntityIdentifier = entityIdentifier; state.selectedEntityIdentifier = entityIdentifier;
}, },
entityNameChanged: (state, action: PayloadAction<EntityIdentifierPayload & { name: string | null }>) => {
const { entityIdentifier, name } = action.payload;
const entity = selectEntity(state, entityIdentifier);
if (!entity) {
return;
}
if (entity.type === 'inpaint_mask') {
// Inpaint mask cannot be renamed
return;
}
entity.name = name;
},
entityReset: (state, action: PayloadAction<EntityIdentifierPayload>) => { entityReset: (state, action: PayloadAction<EntityIdentifierPayload>) => {
const { entityIdentifier } = action.payload; const { entityIdentifier } = action.payload;
const entity = selectEntity(state, entityIdentifier); const entity = selectEntity(state, entityIdentifier);
@ -451,6 +463,7 @@ export const {
rasterizationCachesInvalidated, rasterizationCachesInvalidated,
// All entities // All entities
entitySelected, entitySelected,
entityNameChanged,
entityReset, entityReset,
entityIsEnabledToggled, entityIsEnabledToggled,
entityMoved, entityMoved,

View File

@ -33,6 +33,7 @@ export const controlLayersReducers = {
const { id, overrides, isSelected } = action.payload; const { id, overrides, isSelected } = action.payload;
const layer: CanvasControlLayerState = { const layer: CanvasControlLayerState = {
id, id,
name: null,
type: 'control_layer', type: 'control_layer',
isEnabled: true, isEnabled: true,
objects: [], objects: [],

View File

@ -30,6 +30,7 @@ export const rasterLayersReducers = {
const { id, overrides, isSelected } = action.payload; const { id, overrides, isSelected } = action.payload;
const layer: CanvasRasterLayerState = { const layer: CanvasRasterLayerState = {
id, id,
name: null,
type: 'raster_layer', type: 'raster_layer',
isEnabled: true, isEnabled: true,
objects: [], objects: [],

View File

@ -45,6 +45,7 @@ export const regionsReducers = {
const { id } = action.payload; const { id } = action.payload;
const rg: CanvasRegionalGuidanceState = { const rg: CanvasRegionalGuidanceState = {
id, id,
name: null,
type: 'regional_guidance', type: 'regional_guidance',
isEnabled: true, isEnabled: true,
objects: [], objects: [],

View File

@ -647,6 +647,7 @@ export type ImageCache = z.infer<typeof zImageCache>;
export const zCanvasRegionalGuidanceState = z.object({ export const zCanvasRegionalGuidanceState = z.object({
id: zId, id: zId,
name: z.string().nullable(),
type: z.literal('regional_guidance'), type: z.literal('regional_guidance'),
isEnabled: z.boolean(), isEnabled: z.boolean(),
position: zCoordinate, position: zCoordinate,
@ -730,6 +731,7 @@ export type T2IAdapterConfig = z.infer<typeof zT2IAdapterConfig>;
export const zCanvasRasterLayerState = z.object({ export const zCanvasRasterLayerState = z.object({
id: zId, id: zId,
name: z.string().nullable(),
type: z.literal('raster_layer'), type: z.literal('raster_layer'),
isEnabled: z.boolean(), isEnabled: z.boolean(),
position: zCoordinate, position: zCoordinate,