InvokeAI/invokeai/frontend/web/src/common/components/IAISlider.tsx
2023-04-26 12:40:10 +10:00

256 lines
6.5 KiB
TypeScript

import {
FormControl,
FormControlProps,
FormLabel,
FormLabelProps,
HStack,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputFieldProps,
NumberInputProps,
NumberInputStepper,
NumberInputStepperProps,
Slider,
SliderFilledTrack,
SliderMark,
SliderMarkProps,
SliderThumb,
SliderThumbProps,
SliderTrack,
SliderTrackProps,
Tooltip,
TooltipProps,
} from '@chakra-ui/react';
import { clamp } from 'lodash';
import { useTranslation } from 'react-i18next';
import { FocusEvent, memo, useEffect, useMemo, useState } from 'react';
import { BiReset } from 'react-icons/bi';
import IAIIconButton, { IAIIconButtonProps } from './IAIIconButton';
import { roundDownToMultiple } from 'common/util/roundDownToMultiple';
export type IAIFullSliderProps = {
label: string;
value: number;
min?: number;
max?: number;
step?: number;
onChange: (v: number) => void;
withSliderMarks?: boolean;
withInput?: boolean;
isInteger?: boolean;
inputWidth?: string | number;
inputReadOnly?: boolean;
withReset?: boolean;
handleReset?: () => void;
tooltipSuffix?: string;
hideTooltip?: boolean;
isCompact?: boolean;
isDisabled?: boolean;
sliderFormControlProps?: FormControlProps;
sliderFormLabelProps?: FormLabelProps;
sliderMarkProps?: Omit<SliderMarkProps, 'value'>;
sliderTrackProps?: SliderTrackProps;
sliderThumbProps?: SliderThumbProps;
sliderNumberInputProps?: NumberInputProps;
sliderNumberInputFieldProps?: NumberInputFieldProps;
sliderNumberInputStepperProps?: NumberInputStepperProps;
sliderTooltipProps?: Omit<TooltipProps, 'children'>;
sliderIAIIconButtonProps?: IAIIconButtonProps;
};
const IAISlider = (props: IAIFullSliderProps) => {
const [showTooltip, setShowTooltip] = useState(false);
const {
label,
value,
min = 1,
max = 100,
step = 1,
onChange,
tooltipSuffix = '',
withSliderMarks = false,
withInput = false,
isInteger = false,
inputWidth = 16,
inputReadOnly = false,
withReset = false,
hideTooltip = false,
isCompact = false,
isDisabled = false,
handleReset,
sliderFormControlProps,
sliderFormLabelProps,
sliderMarkProps,
sliderTrackProps,
sliderThumbProps,
sliderNumberInputProps,
sliderNumberInputFieldProps,
sliderNumberInputStepperProps,
sliderTooltipProps,
sliderIAIIconButtonProps,
...rest
} = props;
const { t } = useTranslation();
const [localInputValue, setLocalInputValue] = useState<
string | number | undefined
>(String(value));
useEffect(() => {
setLocalInputValue(value);
}, [value]);
const numberInputMax = useMemo(
() => (sliderNumberInputProps?.max ? sliderNumberInputProps.max : max),
[max, sliderNumberInputProps?.max]
);
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(localInputValue),
min,
numberInputMax
);
const quantized = roundDownToMultiple(clamped, step);
onChange(quantized);
setLocalInputValue(quantized);
};
const handleInputChange = (v: number | string) => {
setLocalInputValue(v);
};
const handleResetDisable = () => {
if (!handleReset) return;
handleReset();
};
return (
<FormControl
sx={
isCompact
? {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
columnGap: 4,
margin: 0,
padding: 0,
}
: {}
}
isDisabled={isDisabled}
{...sliderFormControlProps}
>
<FormLabel {...sliderFormLabelProps} mb={-1}>
{label}
</FormLabel>
<HStack w="100%" gap={2} alignItems="center">
<Slider
aria-label={label}
value={value}
min={min}
max={max}
step={step}
onChange={handleSliderChange}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
focusThumbOnChange={false}
isDisabled={isDisabled}
{...rest}
>
{withSliderMarks && (
<>
<SliderMark
value={min}
sx={{
insetInlineStart: '0 !important',
insetInlineEnd: 'unset !important',
}}
{...sliderMarkProps}
>
{min}
</SliderMark>
<SliderMark
value={max}
sx={{
insetInlineStart: 'unset !important',
insetInlineEnd: '0 !important',
}}
{...sliderMarkProps}
>
{max}
</SliderMark>
</>
)}
<SliderTrack {...sliderTrackProps}>
<SliderFilledTrack />
</SliderTrack>
<Tooltip
hasArrow
placement="top"
isOpen={showTooltip}
label={`${value}${tooltipSuffix}`}
hidden={hideTooltip}
{...sliderTooltipProps}
>
<SliderThumb {...sliderThumbProps} />
</Tooltip>
</Slider>
{withInput && (
<NumberInput
min={min}
max={numberInputMax}
step={step}
value={localInputValue}
onChange={handleInputChange}
onBlur={handleInputBlur}
{...sliderNumberInputProps}
>
<NumberInputField
readOnly={inputReadOnly}
minWidth={inputWidth}
{...sliderNumberInputFieldProps}
/>
<NumberInputStepper {...sliderNumberInputStepperProps}>
<NumberIncrementStepper
onClick={() => onChange(Number(localInputValue))}
/>
<NumberDecrementStepper
onClick={() => onChange(Number(localInputValue))}
/>
</NumberInputStepper>
</NumberInput>
)}
{withReset && (
<IAIIconButton
size="sm"
aria-label={t('accessibility.reset')}
tooltip="Reset"
icon={<BiReset />}
isDisabled={isDisabled}
onClick={handleResetDisable}
{...sliderIAIIconButtonProps}
/>
)}
</HStack>
</FormControl>
);
};
export default memo(IAISlider);