feat(ui): rework settings menu

This commit is contained in:
psychedelicious 2024-08-22 18:14:11 +10:00
parent fa48145cbc
commit 82f0cb2c8c
11 changed files with 199 additions and 143 deletions

View File

@ -1645,7 +1645,11 @@
"storeNotInitialized": "Store is not initialized"
},
"controlLayers": {
"resetCanvas": "Reset Canvas",
"resetAll": "Reset All",
"clearCaches": "Clear Caches",
"recalculateRects": "Recalculate Rects",
"clipToBbox": "Clip Strokes to Bbox",
"addLayer": "Add Layer",
"moveToFront": "Move to Front",
"moveToBack": "Move to Back",

View File

@ -1,84 +0,0 @@
import {
Button,
Checkbox,
Flex,
FormControl,
FormLabel,
IconButton,
Popover,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { CanvasSettingsDynamicGridToggle } from 'features/controlLayers/components/CanvasSettingsDynamicGridToggle';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { clipToBboxChanged, invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RiSettings4Fill } from 'react-icons/ri';
const ControlLayersSettingsPopover = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const canvasManager = useStore($canvasManager);
const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox);
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
const onChangeInvertScroll = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
[dispatch]
);
const onChangeClipToBbox = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
[dispatch]
);
const clearCaches = useCallback(() => {
canvasManager?.cache.clearAll();
}, [canvasManager]);
const calculateBboxes = useCallback(() => {
if (!canvasManager) {
return;
}
const adapters = [
...canvasManager.rasterLayerAdapters.values(),
...canvasManager.controlLayerAdapters.values(),
...canvasManager.regionalGuidanceAdapters.values(),
...canvasManager.inpaintMaskAdapters.values(),
];
for (const adapter of adapters) {
adapter.transformer.requestRectCalculation();
}
}, [canvasManager]);
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} />
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<Flex direction="column" gap={2}>
<FormControl w="full">
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
<Checkbox isChecked={invertScroll} onChange={onChangeInvertScroll} />
</FormControl>
<FormControl w="full">
<FormLabel flexGrow={1}>{t('unifiedCanvas.clipToBbox')}</FormLabel>
<Checkbox isChecked={clipToBbox} onChange={onChangeClipToBbox} />
</FormControl>
<CanvasSettingsDynamicGridToggle />
<Button onClick={clearCaches} size="sm">
Clear Caches
</Button>
<Button onClick={calculateBboxes} size="sm">
Calculate Bboxes
</Button>
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default memo(ControlLayersSettingsPopover);

View File

@ -1,63 +1,38 @@
/* eslint-disable i18next/no-literal-string */
import { Flex, Switch } from '@invoke-ai/ui-library';
import { useStore } from '@nanostores/react';
import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { CanvasResetViewButton } from 'features/controlLayers/components/CanvasResetViewButton';
import { CanvasScale } from 'features/controlLayers/components/CanvasScale';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ResetCanvasButton } from 'features/controlLayers/components/ResetCanvasButton';
import { CanvasSettingsPopover } from 'features/controlLayers/components/Settings/CanvasSettingsPopover';
import { ToolBrushWidth } from 'features/controlLayers/components/Tool/ToolBrushWidth';
import { ToolChooser } from 'features/controlLayers/components/Tool/ToolChooser';
import { ToolEraserWidth } from 'features/controlLayers/components/Tool/ToolEraserWidth';
import { ToolFillColorPicker } from 'features/controlLayers/components/Tool/ToolFillColorPicker';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
import { ViewerToggleMenu } from 'features/gallery/components/ImageViewer/ViewerToggleMenu';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { memo } from 'react';
export const ControlLayersToolbar = memo(() => {
const tool = useAppSelector((s) => s.canvasV2.tool.selected);
const canvasManager = useStore($canvasManager);
const onChangeDebugging = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
if (!canvasManager) {
return;
}
if (e.target.checked) {
canvasManager.enableDebugging();
} else {
canvasManager.disableDebugging();
}
},
[canvasManager]
);
return (
<Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto" alignItems="center">
<ToggleProgressButton />
<ToolChooser />
</Flex>
</Flex>
<Flex flex={1} gap={2} justifyContent="center" alignItems="center">
<CanvasManagerProviderGate>
<Flex w="full" gap={2} alignItems="center">
<ToggleProgressButton />
<ToolChooser />
{tool === 'brush' && <ToolBrushWidth />}
{tool === 'eraser' && <ToolEraserWidth />}
<Spacer />
<CanvasScale />
<CanvasResetViewButton />
<Spacer />
<ToolFillColorPicker />
<UndoRedoButtonGroup />
<CanvasSettingsPopover />
<ViewerToggleMenu />
</Flex>
<CanvasScale />
<CanvasResetViewButton />
<Switch onChange={onChangeDebugging}>debug</Switch>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto" alignItems="center">
<ToolFillColorPicker />
<UndoRedoButtonGroup />
<ControlLayersSettingsPopover />
<ResetCanvasButton />
<ViewerToggleMenu />
</Flex>
</Flex>
</Flex>
</CanvasManagerProviderGate>
);
});

View File

@ -1,15 +0,0 @@
import { IconButton } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { canvasReset } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { PiTrashBold } from 'react-icons/pi';
export const ResetCanvasButton = memo(() => {
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(canvasReset());
}, [dispatch]);
return <IconButton onClick={onClick} icon={<PiTrashBold />} aria-label="Reset canvas" colorScheme="error" />;
});
ResetCanvasButton.displayName = 'ResetCanvasButton';

View File

@ -0,0 +1,19 @@
import { Button } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsClearCachesButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const clearCaches = useCallback(() => {
canvasManager.cache.clearAll();
}, [canvasManager]);
return (
<Button onClick={clearCaches} size="sm" colorScheme="warning">
{t('controlLayers.clearCaches')}
</Button>
);
});
CanvasSettingsClearCachesButton.displayName = 'CanvasSettingsClearCachesButton';

View File

@ -0,0 +1,24 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { clipToBboxChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsClipToBboxCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const clipToBbox = useAppSelector((s) => s.canvasV2.settings.clipToBbox);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
[dispatch]
);
return (
<FormControl w="full">
<FormLabel flexGrow={1}>{t('controlLayers.clipToBbox')}</FormLabel>
<Checkbox isChecked={clipToBbox} onChange={onChange} />
</FormControl>
);
});
CanvasSettingsClipToBboxCheckbox.displayName = 'CanvasSettingsClipToBboxCheckbox';

View File

@ -4,7 +4,7 @@ import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasV
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsDynamicGridToggle = memo(() => {
export const CanvasSettingsDynamicGridSwitch = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const dynamicGrid = useAppSelector((s) => s.canvasV2.settings.dynamicGrid);
@ -22,4 +22,4 @@ export const CanvasSettingsDynamicGridToggle = memo(() => {
);
});
CanvasSettingsDynamicGridToggle.displayName = 'CanvasSettingsDynamicGridToggle';
CanvasSettingsDynamicGridSwitch.displayName = 'CanvasSettingsDynamicGridSwitch';

View File

@ -0,0 +1,24 @@
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { invertScrollChanged } from 'features/controlLayers/store/canvasV2Slice';
import type { ChangeEvent } from 'react';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const invertScroll = useAppSelector((s) => s.canvasV2.tool.invertScroll);
const onChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
[dispatch]
);
return (
<FormControl w="full">
<FormLabel flexGrow={1}>{t('unifiedCanvas.invertBrushSizeScrollDirection')}</FormLabel>
<Checkbox isChecked={invertScroll} onChange={onChange} />
</FormControl>
);
});
CanvasSettingsInvertScrollCheckbox.displayName = 'CanvasSettingsInvertScrollCheckbox';

View File

@ -0,0 +1,61 @@
import {
Divider,
Flex,
IconButton,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
useShiftModifier,
} from '@invoke-ai/ui-library';
import { CanvasSettingsClearCachesButton } from 'features/controlLayers/components/Settings/CanvasSettingsClearCachesButton';
import { CanvasSettingsClipToBboxCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox';
import { CanvasSettingsDynamicGridSwitch } from 'features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch';
import { CanvasSettingsInvertScrollCheckbox } from 'features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox';
import { CanvasSettingsRecalculateRectsButton } from 'features/controlLayers/components/Settings/CanvasSettingsRecalculateRectsButton';
import { CanvasSettingsResetButton } from 'features/controlLayers/components/Settings/CanvasSettingsResetButton';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { RiSettings4Fill } from 'react-icons/ri';
export const CanvasSettingsPopover = memo(() => {
const { t } = useTranslation();
return (
<Popover isLazy>
<PopoverTrigger>
<IconButton aria-label={t('common.settingsLabel')} icon={<RiSettings4Fill />} />
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>
<Flex direction="column" gap={2}>
<CanvasSettingsInvertScrollCheckbox />
<CanvasSettingsClipToBboxCheckbox />
<CanvasSettingsDynamicGridSwitch />
<CanvasSettingsResetButton />
<DebugSettings />
</Flex>
</PopoverBody>
</PopoverContent>
</Popover>
);
});
CanvasSettingsPopover.displayName = 'CanvasSettingsPopover';
const DebugSettings = () => {
const shift = useShiftModifier();
if (!shift) {
return null;
}
return (
<>
<Divider />
<CanvasSettingsClearCachesButton />
<CanvasSettingsRecalculateRectsButton />
</>
);
};

View File

@ -0,0 +1,28 @@
import { Button } from '@invoke-ai/ui-library';
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsRecalculateRectsButton = memo(() => {
const { t } = useTranslation();
const canvasManager = useCanvasManager();
const onClick = useCallback(() => {
const adapters = [
...canvasManager.rasterLayerAdapters.values(),
...canvasManager.controlLayerAdapters.values(),
...canvasManager.regionalGuidanceAdapters.values(),
...canvasManager.inpaintMaskAdapters.values(),
];
for (const adapter of adapters) {
adapter.transformer.requestRectCalculation();
}
}, [canvasManager]);
return (
<Button onClick={onClick} size="sm" colorScheme="warning">
{t('controlLayers.recalculateRects')}
</Button>
);
});
CanvasSettingsRecalculateRectsButton.displayName = 'CanvasSettingsRecalculateRectsButton';

View File

@ -0,0 +1,20 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppDispatch } from 'app/store/storeHooks';
import { canvasReset } from 'features/controlLayers/store/canvasV2Slice';
import { memo, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
export const CanvasSettingsResetButton = memo(() => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const onClick = useCallback(() => {
dispatch(canvasReset());
}, [dispatch]);
return (
<Button onClick={onClick} colorScheme="error" size="sm">
{t('controlLayers.resetCanvas')}
</Button>
);
});
CanvasSettingsResetButton.displayName = 'CanvasSettingsResetButton';