mirror of
https://github.com/invoke-ai/InvokeAI.git
synced 2026-04-23 03:00:31 -04:00
perf(ui): use narrow selectors in adjustments to reduce rerenders
dramatically improves the feel of the sliders
This commit is contained in:
@@ -7,44 +7,56 @@ import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerP
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import {
|
||||
rasterLayerAdjustmentsCancel,
|
||||
rasterLayerAdjustmentsCollapsedToggled,
|
||||
rasterLayerAdjustmentsEnabledToggled,
|
||||
rasterLayerAdjustmentsModeChanged,
|
||||
rasterLayerAdjustmentsReset,
|
||||
rasterLayerAdjustmentsSet,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||
import { makeDefaultRasterLayerAdjustments } from 'features/controlLayers/store/util';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiArrowCounterClockwiseBold, PiCaretDownBold, PiCheckBold, PiTrashBold } from 'react-icons/pi';
|
||||
|
||||
export const RasterLayerAdjustmentsPanel = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
|
||||
const canvasManager = useCanvasManager();
|
||||
const selectAdjustments = useMemo(() => {
|
||||
return createSelector(selectCanvasSlice, (canvas) => selectEntity(canvas, entityIdentifier)?.adjustments);
|
||||
|
||||
const selectHasAdjustments = useMemo(() => {
|
||||
return createSelector(selectCanvasSlice, (canvas) => Boolean(selectEntity(canvas, entityIdentifier)?.adjustments));
|
||||
}, [entityIdentifier]);
|
||||
|
||||
const adjustments = useAppSelector(selectAdjustments);
|
||||
const { t } = useTranslation();
|
||||
const hasAdjustments = useAppSelector(selectHasAdjustments);
|
||||
|
||||
const hasAdjustments = Boolean(adjustments);
|
||||
const enabled = Boolean(adjustments?.enabled);
|
||||
const collapsed = Boolean(adjustments?.collapsed);
|
||||
const mode = adjustments?.mode ?? 'simple';
|
||||
const selectMode = useMemo(() => {
|
||||
return createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.mode ?? 'simple'
|
||||
);
|
||||
}, [entityIdentifier]);
|
||||
const mode = useAppSelector(selectMode);
|
||||
|
||||
const onToggleEnabled = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const v = e.target.checked;
|
||||
const current = adjustments ?? makeDefaultRasterLayerAdjustments(mode);
|
||||
dispatch(
|
||||
rasterLayerAdjustmentsSet({
|
||||
entityIdentifier,
|
||||
adjustments: { ...current, enabled: v },
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, entityIdentifier, adjustments, mode]
|
||||
);
|
||||
const selectEnabled = useMemo(() => {
|
||||
return createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.enabled ?? false
|
||||
);
|
||||
}, [entityIdentifier]);
|
||||
const enabled = useAppSelector(selectEnabled);
|
||||
|
||||
const selectCollapsed = useMemo(() => {
|
||||
return createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.collapsed ?? false
|
||||
);
|
||||
}, [entityIdentifier]);
|
||||
const collapsed = useAppSelector(selectCollapsed);
|
||||
|
||||
const onToggleEnabled = useCallback(() => {
|
||||
dispatch(rasterLayerAdjustmentsEnabledToggled({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
const onReset = useCallback(() => {
|
||||
// Reset values to defaults but keep adjustments present; preserve enabled/collapsed/mode
|
||||
@@ -57,34 +69,18 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
const onToggleCollapsed = useCallback(() => {
|
||||
const current = adjustments ?? makeDefaultRasterLayerAdjustments(mode);
|
||||
dispatch(
|
||||
rasterLayerAdjustmentsSet({
|
||||
entityIdentifier,
|
||||
adjustments: { ...current, collapsed: !collapsed },
|
||||
})
|
||||
);
|
||||
}, [dispatch, entityIdentifier, collapsed, adjustments, mode]);
|
||||
dispatch(rasterLayerAdjustmentsCollapsedToggled({ entityIdentifier }));
|
||||
}, [dispatch, entityIdentifier]);
|
||||
|
||||
const onSetMode = useCallback(
|
||||
(nextMode: 'simple' | 'curves') => {
|
||||
if (nextMode === mode) {
|
||||
return;
|
||||
}
|
||||
const current = adjustments ?? makeDefaultRasterLayerAdjustments(nextMode);
|
||||
dispatch(
|
||||
rasterLayerAdjustmentsSet({
|
||||
entityIdentifier,
|
||||
adjustments: { ...current, mode: nextMode },
|
||||
})
|
||||
);
|
||||
},
|
||||
[dispatch, entityIdentifier, adjustments, mode]
|
||||
const onClickModeSimple = useCallback(
|
||||
() => dispatch(rasterLayerAdjustmentsModeChanged({ entityIdentifier, mode: 'simple' })),
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
// Memoized click handlers to avoid inline arrow functions in JSX
|
||||
const onClickModeSimple = useCallback(() => onSetMode('simple'), [onSetMode]);
|
||||
const onClickModeCurves = useCallback(() => onSetMode('curves'), [onSetMode]);
|
||||
const onClickModeCurves = useCallback(
|
||||
() => dispatch(rasterLayerAdjustmentsModeChanged({ entityIdentifier, mode: 'curves' })),
|
||||
[dispatch, entityIdentifier]
|
||||
);
|
||||
|
||||
const onFinish = useCallback(async () => {
|
||||
// Bake current visual into layer pixels, then clear adjustments
|
||||
@@ -137,7 +133,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
|
||||
aria-label={t('controlLayers.adjustments.cancel')}
|
||||
size="md"
|
||||
onClick={onCancel}
|
||||
isDisabled={!adjustments}
|
||||
isDisabled={!hasAdjustments}
|
||||
colorScheme="red"
|
||||
icon={<PiTrashBold />}
|
||||
variant="ghost"
|
||||
@@ -146,7 +142,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
|
||||
aria-label={t('controlLayers.adjustments.reset')}
|
||||
size="md"
|
||||
onClick={onReset}
|
||||
isDisabled={!adjustments}
|
||||
isDisabled={!hasAdjustments}
|
||||
icon={<PiArrowCounterClockwiseBold />}
|
||||
variant="ghost"
|
||||
/>
|
||||
@@ -154,7 +150,7 @@ export const RasterLayerAdjustmentsPanel = memo(() => {
|
||||
aria-label={t('controlLayers.adjustments.finish')}
|
||||
size="md"
|
||||
onClick={onFinish}
|
||||
isDisabled={!adjustments}
|
||||
isDisabled={!hasAdjustments}
|
||||
colorScheme="green"
|
||||
icon={<PiCheckBold />}
|
||||
variant="ghost"
|
||||
|
||||
@@ -4,27 +4,40 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { rasterLayerAdjustmentsSimpleUpdated } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice, selectEntity } from 'features/controlLayers/store/selectors';
|
||||
import type { SimpleAdjustmentsConfig } from 'features/controlLayers/store/types';
|
||||
import React, { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
type AdjustmentSliderRowProps = {
|
||||
label: string;
|
||||
value: number;
|
||||
name: keyof SimpleAdjustmentsConfig;
|
||||
onChange: (v: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
const AdjustmentSliderRow = ({ label, value, onChange, min = -1, max = 1, step = 0.01 }: AdjustmentSliderRowProps) => (
|
||||
<FormControl orientation="horizontal" mb={1} w="full">
|
||||
<FormLabel m={0} minW="90px">
|
||||
{label}
|
||||
</FormLabel>
|
||||
<CompositeSlider value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} marks />
|
||||
<CompositeNumberInput value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} />
|
||||
</FormControl>
|
||||
);
|
||||
const AdjustmentSliderRow = ({ label, name, onChange, min = -1, max = 1, step = 0.01 }: AdjustmentSliderRowProps) => {
|
||||
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
|
||||
const selectValue = useMemo(() => {
|
||||
return createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) =>
|
||||
selectEntity(canvas, entityIdentifier)?.adjustments?.simple?.[name] ?? DEFAULT_SIMPLE_ADJUSTMENTS[name]
|
||||
);
|
||||
}, [entityIdentifier, name]);
|
||||
const value = useAppSelector(selectValue);
|
||||
|
||||
return (
|
||||
<FormControl orientation="horizontal" mb={1} w="full">
|
||||
<FormLabel m={0} minW="90px">
|
||||
{label}
|
||||
</FormLabel>
|
||||
<CompositeSlider value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} marks />
|
||||
<CompositeNumberInput value={value} onChange={onChange} defaultValue={0} min={min} max={max} step={step} />
|
||||
</FormControl>
|
||||
);
|
||||
};
|
||||
|
||||
const DEFAULT_SIMPLE_ADJUSTMENTS = {
|
||||
brightness: 0,
|
||||
@@ -39,13 +52,6 @@ export const RasterLayerSimpleAdjustmentsEditor = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const entityIdentifier = useEntityIdentifierContext<'raster_layer'>();
|
||||
const { t } = useTranslation();
|
||||
const selectSimpleAdjustments = useMemo(() => {
|
||||
return createSelector(
|
||||
selectCanvasSlice,
|
||||
(canvas) => selectEntity(canvas, entityIdentifier)?.adjustments?.simple ?? DEFAULT_SIMPLE_ADJUSTMENTS
|
||||
);
|
||||
}, [entityIdentifier]);
|
||||
const simple = useAppSelector(selectSimpleAdjustments);
|
||||
const selectIsDisabled = useMemo(() => {
|
||||
return createSelector(
|
||||
selectCanvasSlice,
|
||||
@@ -83,28 +89,24 @@ export const RasterLayerSimpleAdjustmentsEditor = memo(() => {
|
||||
<Flex px={3} pb={2} direction="column" opacity={isDisabled ? 0.3 : 1} pointerEvents={isDisabled ? 'none' : 'auto'}>
|
||||
<AdjustmentSliderRow
|
||||
label={t('controlLayers.adjustments.brightness')}
|
||||
value={simple.brightness}
|
||||
name="brightness"
|
||||
onChange={onBrightness}
|
||||
/>
|
||||
<AdjustmentSliderRow
|
||||
label={t('controlLayers.adjustments.contrast')}
|
||||
value={simple.contrast}
|
||||
onChange={onContrast}
|
||||
/>
|
||||
<AdjustmentSliderRow label={t('controlLayers.adjustments.contrast')} name="contrast" onChange={onContrast} />
|
||||
<AdjustmentSliderRow
|
||||
label={t('controlLayers.adjustments.saturation')}
|
||||
value={simple.saturation}
|
||||
name="saturation"
|
||||
onChange={onSaturation}
|
||||
/>
|
||||
<AdjustmentSliderRow
|
||||
label={t('controlLayers.adjustments.temperature')}
|
||||
value={simple.temperature}
|
||||
name="temperature"
|
||||
onChange={onTemperature}
|
||||
/>
|
||||
<AdjustmentSliderRow label={t('controlLayers.adjustments.tint')} value={simple.tint} onChange={onTint} />
|
||||
<AdjustmentSliderRow label={t('controlLayers.adjustments.tint')} name="tint" onChange={onTint} />
|
||||
<AdjustmentSliderRow
|
||||
label={t('controlLayers.adjustments.sharpness')}
|
||||
value={simple.sharpness}
|
||||
name="sharpness"
|
||||
onChange={onSharpness}
|
||||
min={0}
|
||||
max={1}
|
||||
|
||||
@@ -130,13 +130,11 @@ const slice = createSlice({
|
||||
rasterLayerAdjustmentsReset: (state, action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
const layer = selectEntity(state, entityIdentifier);
|
||||
if (!layer) {
|
||||
if (!layer?.adjustments) {
|
||||
return;
|
||||
}
|
||||
if (layer.adjustments) {
|
||||
layer.adjustments.simple = makeDefaultRasterLayerAdjustments('simple').simple;
|
||||
layer.adjustments.curves = makeDefaultRasterLayerAdjustments('curves').curves;
|
||||
}
|
||||
layer.adjustments.simple = makeDefaultRasterLayerAdjustments('simple').simple;
|
||||
layer.adjustments.curves = makeDefaultRasterLayerAdjustments('curves').curves;
|
||||
},
|
||||
rasterLayerAdjustmentsCancel: (state, action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
@@ -146,18 +144,26 @@ const slice = createSlice({
|
||||
}
|
||||
delete layer.adjustments;
|
||||
},
|
||||
rasterLayerAdjustmentsModeChanged: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ mode: 'simple' | 'curves' }, 'raster_layer'>>
|
||||
) => {
|
||||
const { entityIdentifier, mode } = action.payload;
|
||||
const layer = selectEntity(state, entityIdentifier);
|
||||
if (!layer?.adjustments) {
|
||||
return;
|
||||
}
|
||||
layer.adjustments.mode = mode;
|
||||
},
|
||||
rasterLayerAdjustmentsSimpleUpdated: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<{ simple: Partial<SimpleAdjustmentsConfig> }, 'raster_layer'>>
|
||||
) => {
|
||||
const { entityIdentifier, simple } = action.payload;
|
||||
const layer = selectEntity(state, entityIdentifier);
|
||||
if (!layer) {
|
||||
if (!layer?.adjustments) {
|
||||
return;
|
||||
}
|
||||
if (!layer.adjustments) {
|
||||
layer.adjustments = makeDefaultRasterLayerAdjustments('simple');
|
||||
}
|
||||
layer.adjustments.simple = merge(layer.adjustments.simple, simple);
|
||||
},
|
||||
rasterLayerAdjustmentsCurvesUpdated: (
|
||||
@@ -166,14 +172,33 @@ const slice = createSlice({
|
||||
) => {
|
||||
const { entityIdentifier, channel, points } = action.payload;
|
||||
const layer = selectEntity(state, entityIdentifier);
|
||||
if (!layer) {
|
||||
if (!layer?.adjustments) {
|
||||
return;
|
||||
}
|
||||
if (!layer.adjustments) {
|
||||
layer.adjustments = makeDefaultRasterLayerAdjustments('curves');
|
||||
}
|
||||
layer.adjustments.curves[channel] = points;
|
||||
},
|
||||
rasterLayerAdjustmentsEnabledToggled: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>
|
||||
) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
const layer = selectEntity(state, entityIdentifier);
|
||||
if (!layer?.adjustments) {
|
||||
return;
|
||||
}
|
||||
layer.adjustments.enabled = !layer.adjustments.enabled;
|
||||
},
|
||||
rasterLayerAdjustmentsCollapsedToggled: (
|
||||
state,
|
||||
action: PayloadAction<EntityIdentifierPayload<void, 'raster_layer'>>
|
||||
) => {
|
||||
const { entityIdentifier } = action.payload;
|
||||
const layer = selectEntity(state, entityIdentifier);
|
||||
if (!layer?.adjustments) {
|
||||
return;
|
||||
}
|
||||
layer.adjustments.collapsed = !layer.adjustments.collapsed;
|
||||
},
|
||||
rasterLayerAdded: {
|
||||
reducer: (
|
||||
state,
|
||||
@@ -1732,6 +1757,9 @@ export const {
|
||||
rasterLayerAdjustmentsSet,
|
||||
rasterLayerAdjustmentsCancel,
|
||||
rasterLayerAdjustmentsReset,
|
||||
rasterLayerAdjustmentsModeChanged,
|
||||
rasterLayerAdjustmentsEnabledToggled,
|
||||
rasterLayerAdjustmentsCollapsedToggled,
|
||||
rasterLayerAdjustmentsSimpleUpdated,
|
||||
rasterLayerAdjustmentsCurvesUpdated,
|
||||
entityDeleted,
|
||||
|
||||
Reference in New Issue
Block a user