perf(ui): use narrow selectors in adjustments to reduce rerenders

dramatically improves the feel of the sliders
This commit is contained in:
psychedelicious
2025-09-11 16:33:30 +10:00
parent 7273700f61
commit e768a3bc7b
3 changed files with 116 additions and 90 deletions

View File

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

View File

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

View File

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