Fix Sliders unable to take typed input (#2407)

So far the slider component was unable to take typed input due to a
bunch of issues that were a pain to solve. This PR fixes it.

Things to test:

- Moving the slider also updates the value in the input text box.
- Input text box next to slider can be changed in two ways: If you type
a manual value, the slider will be updated when you lose focus from the
input box. If you use the stepper icons to update the values, the slider
should update immediately.
- Make sure the reset buttons next to the slider are updating correctly
and make sure this updates both the slider and the input box values.
- Brush Size slider -> make sure the hotkeys are updating the input box
too.
This commit is contained in:
Lincoln Stein 2023-01-26 17:10:16 -05:00 committed by GitHub
commit f36114eb94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 239 additions and 189 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>InvokeAI - A Stable Diffusion Toolkit</title>
<link rel="shortcut icon" type="icon" href="./assets/favicon.0d253ced.ico" />
<script type="module" crossorigin src="./assets/index.d5dcf0c5.js"></script>
<script type="module" crossorigin src="./assets/index.1d13f8a7.js"></script>
<link rel="stylesheet" href="./assets/index.8badc8b4.css">
<script type="module">try{import.meta.url;import("_").catch(()=>1);}catch(e){}window.__vite_is_modern_browser=true;</script>
<script type="module">!function(){if(window.__vite_is_modern_browser)return;console.warn("vite: loading legacy build because dynamic import or import.meta.url is unsupported, syntax error above should be ignored");var e=document.getElementById("vite-legacy-polyfill"),n=document.createElement("script");n.src=e.src,n.onload=function(){System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))},document.body.appendChild(n)}();</script>
@ -18,6 +18,6 @@
<script nomodule>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",(function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()}),!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script>
<script nomodule crossorigin id="vite-legacy-polyfill" src="./assets/polyfills-legacy-dde3a68a.js"></script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="./assets/index-legacy-dad2eee9.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
<script nomodule crossorigin id="vite-legacy-entry" data-src="./assets/index-legacy-65b0dda3.js">System.import(document.getElementById('vite-legacy-entry').getAttribute('data-src'))</script>
</body>
</html>

View File

@ -23,7 +23,7 @@ import {
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import React, { FocusEvent, useEffect, useMemo, useState } from 'react';
import React, { FocusEvent, useMemo, useState, useEffect } from 'react';
import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
import _ from 'lodash';
@ -81,7 +81,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
withInput = false,
isInteger = false,
inputWidth = '5.5rem',
inputReadOnly = true,
inputReadOnly = false,
withReset = false,
hideTooltip = false,
isCompact = false,
@ -103,32 +103,35 @@ export default function IAISlider(props: IAIFullSliderProps) {
...rest
} = props;
const [localInputValue, setLocalInputValue] = useState<string>(String(value));
const [localInputValue, setLocalInputValue] = useState<
string | number | undefined
>(String(value));
useEffect(() => {
setLocalInputValue(value);
}, [value]);
const numberInputMax = useMemo(
() => (sliderNumberInputProps?.max ? sliderNumberInputProps.max : max),
[max, sliderNumberInputProps?.max]
);
useEffect(() => {
if (String(value) !== localInputValue && localInputValue !== '') {
setLocalInputValue(String(value));
}
}, [value, localInputValue, setLocalInputValue]);
const handleSliderChange = (v: number) => {
onChange(v);
};
const handleInputBlur = (e: FocusEvent<HTMLInputElement>) => {
if (e.target.value === '') e.target.value = String(min);
const clamped = _.clamp(
isInteger ? Math.floor(Number(e.target.value)) : Number(e.target.value),
isInteger ? Math.floor(Number(e.target.value)) : Number(localInputValue),
min,
numberInputMax
);
setLocalInputValue(String(clamped));
onChange(clamped);
};
const handleInputChange = (v: number | string) => {
setLocalInputValue(String(v));
onChange(Number(v));
setLocalInputValue(v);
};
const handleResetDisable = () => {
@ -172,7 +175,7 @@ export default function IAISlider(props: IAIFullSliderProps) {
min={min}
max={max}
step={step}
onChange={handleInputChange}
onChange={handleSliderChange}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
focusThumbOnChange={false}
@ -236,13 +239,19 @@ export default function IAISlider(props: IAIFullSliderProps) {
<NumberInputField
className="invokeai__slider-number-input"
width={inputWidth}
minWidth={inputWidth}
readOnly={inputReadOnly}
minWidth={inputWidth}
{...sliderNumberInputFieldProps}
/>
<NumberInputStepper {...sliderNumberInputStepperProps}>
<NumberIncrementStepper className="invokeai__slider-number-stepper" />
<NumberDecrementStepper className="invokeai__slider-number-stepper" />
<NumberIncrementStepper
onClick={() => onChange(Number(localInputValue))}
className="invokeai__slider-number-stepper"
/>
<NumberDecrementStepper
onClick={() => onChange(Number(localInputValue))}
className="invokeai__slider-number-stepper"
/>
</NumberInputStepper>
</NumberInput>
)}

View File

@ -75,11 +75,12 @@ const BoundingBoxSettings = () => {
step={64}
value={boundingBoxDimensions.width}
onChange={handleChangeWidth}
handleReset={handleResetWidth}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetWidth}
/>
<IAISlider
label={t('options:height')}
@ -88,11 +89,12 @@ const BoundingBoxSettings = () => {
step={64}
value={boundingBoxDimensions.height}
onChange={handleChangeHeight}
handleReset={handleResetHeight}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetHeight}
/>
</Flex>
);

View File

@ -124,11 +124,12 @@ const InfillAndScalingOptions = () => {
step={64}
value={scaledBoundingBoxDimensions.width}
onChange={handleChangeScaledWidth}
handleReset={handleResetScaledWidth}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetScaledWidth}
/>
<IAISlider
isInputDisabled={!isManual}
@ -140,11 +141,12 @@ const InfillAndScalingOptions = () => {
step={64}
value={scaledBoundingBoxDimensions.height}
onChange={handleChangeScaledHeight}
handleReset={handleResetScaledHeight}
sliderNumberInputProps={{ max: 4096 }}
withSliderMarks
withInput
inputReadOnly
withReset
handleReset={handleResetScaledHeight}
/>
<InpaintReplace />
<IAISelect
@ -166,12 +168,12 @@ const InfillAndScalingOptions = () => {
onChange={(v) => {
dispatch(setTileSize(v));
}}
handleReset={() => {
dispatch(setTileSize(32));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setTileSize(32));
}}
/>
</Flex>
);

View File

@ -36,7 +36,7 @@ export default function InpaintReplace() {
const { t } = useTranslation();
return (
<Flex alignItems={'center'} columnGap={'1rem'}>
<Flex alignItems={'center'} columnGap={'0.2rem'}>
<IAISlider
label={t('options:inpaintReplace')}
value={inpaintReplace}
@ -51,7 +51,8 @@ export default function InpaintReplace() {
withSliderMarks
sliderMarkRightOffset={-2}
withReset
handleReset={() => dispatch(setInpaintReplace(1))}
handleReset={() => dispatch(setInpaintReplace(0.1))}
withInput
isResetDisabled={!shouldUseInpaintReplace}
/>
<IAISwitch

View File

@ -1,113 +0,0 @@
import { Flex } from '@chakra-ui/react';
import { createSelector } from '@reduxjs/toolkit';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { optionsSelector } from 'features/options/store/optionsSelectors';
import {
setSeamBlur,
setSeamSize,
setSeamSteps,
setSeamStrength,
} from 'features/options/store/optionsSlice';
import _ from 'lodash';
import { useTranslation } from 'react-i18next';
const selector = createSelector(
[optionsSelector],
(options) => {
const { seamSize, seamBlur, seamStrength, seamSteps } = options;
return {
seamSize,
seamBlur,
seamStrength,
seamSteps,
};
},
{
memoizeOptions: {
resultEqualityCheck: _.isEqual,
},
}
);
const SeamCorrectionOptions = () => {
const dispatch = useAppDispatch();
const { seamSize, seamBlur, seamStrength, seamSteps } =
useAppSelector(selector);
const { t } = useTranslation();
return (
<Flex direction="column" gap="1rem">
<IAISlider
sliderMarkRightOffset={-6}
label={t('options:seamSize')}
min={1}
max={256}
sliderNumberInputProps={{ max: 512 }}
value={seamSize}
onChange={(v) => {
dispatch(setSeamSize(v));
}}
handleReset={() => dispatch(setSeamSize(96))}
withInput
withSliderMarks
withReset
/>
<IAISlider
sliderMarkRightOffset={-4}
label={t('options:seamBlur')}
min={0}
max={64}
sliderNumberInputProps={{ max: 512 }}
value={seamBlur}
onChange={(v) => {
dispatch(setSeamBlur(v));
}}
handleReset={() => {
dispatch(setSeamBlur(16));
}}
withInput
withSliderMarks
withReset
/>
<IAISlider
sliderMarkRightOffset={-7}
label={t('options:seamStrength')}
min={0.01}
max={0.99}
step={0.01}
value={seamStrength}
onChange={(v) => {
dispatch(setSeamStrength(v));
}}
handleReset={() => {
dispatch(setSeamStrength(0.7));
}}
withInput
withSliderMarks
withReset
/>
<IAISlider
sliderMarkRightOffset={-4}
label={t('options:seamSteps')}
min={1}
max={32}
sliderNumberInputProps={{ max: 100 }}
value={seamSteps}
onChange={(v) => {
dispatch(setSeamSteps(v));
}}
handleReset={() => {
dispatch(setSeamSteps(10));
}}
withInput
withSliderMarks
withReset
/>
</Flex>
);
};
export default SeamCorrectionOptions;

View File

@ -0,0 +1,32 @@
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamBlur } from 'features/options/store/optionsSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamBlur() {
const dispatch = useAppDispatch();
const seamBlur = useAppSelector((state: RootState) => state.options.seamBlur);
const { t } = useTranslation();
return (
<IAISlider
sliderMarkRightOffset={-4}
label={t('options:seamBlur')}
min={0}
max={64}
sliderNumberInputProps={{ max: 512 }}
value={seamBlur}
onChange={(v) => {
dispatch(setSeamBlur(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setSeamBlur(16));
}}
/>
);
}

View File

@ -0,0 +1,18 @@
import { Flex } from '@chakra-ui/react';
import SeamBlur from './SeamBlur';
import SeamSize from './SeamSize';
import SeamSteps from './SeamSteps';
import SeamStrength from './SeamStrength';
const SeamCorrectionOptions = () => {
return (
<Flex direction="column" gap="1rem">
<SeamSize />
<SeamBlur />
<SeamStrength />
<SeamSteps />
</Flex>
);
};
export default SeamCorrectionOptions;

View File

@ -0,0 +1,31 @@
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamSize } from 'features/options/store/optionsSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamSize() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const seamSize = useAppSelector((state: RootState) => state.options.seamSize);
return (
<IAISlider
sliderMarkRightOffset={-6}
label={t('options:seamSize')}
min={1}
max={256}
sliderNumberInputProps={{ max: 512 }}
value={seamSize}
onChange={(v) => {
dispatch(setSeamSize(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => dispatch(setSeamSize(96))}
/>
);
}

View File

@ -0,0 +1,34 @@
import type { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamSteps } from 'features/options/store/optionsSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamSteps() {
const { t } = useTranslation();
const seamSteps = useAppSelector(
(state: RootState) => state.options.seamSteps
);
const dispatch = useAppDispatch();
return (
<IAISlider
sliderMarkRightOffset={-4}
label={t('options:seamSteps')}
min={1}
max={32}
sliderNumberInputProps={{ max: 999 }}
value={seamSteps}
onChange={(v) => {
dispatch(setSeamSteps(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setSeamSteps(10));
}}
/>
);
}

View File

@ -0,0 +1,34 @@
import { RootState } from 'app/store';
import { useAppDispatch, useAppSelector } from 'app/storeHooks';
import IAISlider from 'common/components/IAISlider';
import { setSeamStrength } from 'features/options/store/optionsSlice';
import React from 'react';
import { useTranslation } from 'react-i18next';
export default function SeamStrength() {
const dispatch = useAppDispatch();
const { t } = useTranslation();
const seamStrength = useAppSelector(
(state: RootState) => state.options.seamStrength
);
return (
<IAISlider
sliderMarkRightOffset={-7}
label={t('options:seamStrength')}
min={0.01}
max={0.99}
step={0.01}
value={seamStrength}
onChange={(v) => {
dispatch(setSeamStrength(v));
}}
withInput
withSliderMarks
withReset
handleReset={() => {
dispatch(setSeamStrength(0.7));
}}
/>
);
}

View File

@ -36,9 +36,9 @@ export default function ImageToImageStrength(props: ImageToImageStrengthProps) {
isInteger={false}
styleClass={styleClass}
withInput
withReset
withSliderMarks
inputWidth={'5.5rem'}
withReset
handleReset={handleImg2ImgStrengthReset}
/>
);

View File

@ -1,7 +1,7 @@
// import { Feature } from 'app/features';
import { Feature } from 'app/features';
import ImageToImageStrength from 'features/options/components/AdvancedOptions/ImageToImage/ImageToImageStrength';
import SeamCorrectionOptions from 'features/options/components/AdvancedOptions/Canvas/SeamCorrectionOptions';
import SeamCorrectionOptions from 'features/options/components/AdvancedOptions/Canvas/SeamCorrectionOptions/SeamCorrectionOptions';
import SeedOptions from 'features/options/components/AdvancedOptions/Seed/SeedOptions';
import GenerateVariationsToggle from 'features/options/components/AdvancedOptions/Variations/GenerateVariations';
import VariationsOptions from 'features/options/components/AdvancedOptions/Variations/VariationsOptions';