|
1 | 1 | import type { ChakraProps } from '@invoke-ai/ui-library'; |
2 | | -import { Box, CompositeNumberInput, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; |
| 2 | +import { Box, Button, CompositeNumberInput, Flex, FormControl, FormLabel, Input } from '@invoke-ai/ui-library'; |
3 | 3 | import { RGBA_COLOR_SWATCHES } from 'common/components/ColorPicker/swatches'; |
4 | | -import { rgbaColorToString } from 'common/util/colorCodeTransformers'; |
5 | | -import type { CSSProperties } from 'react'; |
6 | | -import { memo, useCallback } from 'react'; |
| 4 | +import { hexToRGBA, rgbaColorToString, rgbaToHex } from 'common/util/colorCodeTransformers'; |
| 5 | +import type { ChangeEvent, CSSProperties } from 'react'; |
| 6 | +import { memo, useCallback, useEffect, useState } from 'react'; |
7 | 7 | import { RgbaColorPicker as ColorfulRgbaColorPicker } from 'react-colorful'; |
8 | 8 | import type { RgbaColor } from 'react-colorful/dist/types'; |
9 | 9 | import { useTranslation } from 'react-i18next'; |
@@ -40,61 +40,131 @@ const RgbaColorPicker = (props: Props) => { |
40 | 40 | const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]); |
41 | 41 | const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]); |
42 | 42 | const handleChangeA = useCallback((a: number) => onChange({ ...color, a }), [color, onChange]); |
| 43 | + const [mode, setMode] = useState<'rgb' | 'hex'>('rgb'); |
| 44 | + const [hex, setHex] = useState<string>(rgbaToHex(color, true)); |
| 45 | + useEffect(() => { |
| 46 | + setHex(rgbaToHex(color, true)); |
| 47 | + }, [color]); |
| 48 | + const onToggleMode = useCallback(() => setMode((m) => (m === 'rgb' ? 'hex' : 'rgb')), []); |
| 49 | + const onChangeHex = useCallback( |
| 50 | + (e: ChangeEvent<HTMLInputElement>) => { |
| 51 | + let value = e.target.value.trim(); |
| 52 | + if (!value.startsWith('#')) { |
| 53 | + value = `#${value}`; |
| 54 | + } |
| 55 | + const cleaned = value.replace(/[^#0-9a-fA-F]/g, '').slice(0, 9); |
| 56 | + setHex(cleaned); |
| 57 | + const hexBody = cleaned.replace('#', ''); |
| 58 | + if (hexBody.length === 6 || hexBody.length === 8) { |
| 59 | + const a = hexBody.length === 8 ? parseInt(hexBody.slice(6, 8), 16) / 255 : color.a; |
| 60 | + const next = hexToRGBA(hexBody.slice(0, 6).padEnd(6, '0'), a); |
| 61 | + onChange(next); |
| 62 | + } |
| 63 | + }, |
| 64 | + [color.a, onChange] |
| 65 | + ); |
| 66 | + const onChangeAlpha = useCallback( |
| 67 | + (a: number) => { |
| 68 | + const next = { ...color, a: Math.max(0, Math.min(1, a)) }; |
| 69 | + onChange(next); |
| 70 | + setHex(rgbaToHex(next, true)); |
| 71 | + }, |
| 72 | + [color, onChange] |
| 73 | + ); |
43 | 74 | return ( |
44 | 75 | <Flex sx={sx}> |
45 | 76 | <ColorfulRgbaColorPicker color={color} onChange={onChange} style={colorPickerStyles} /> |
46 | | - {withNumberInput && ( |
47 | | - <Flex gap={2}> |
48 | | - <FormControl gap={0}> |
49 | | - <FormLabel>{t('common.red')[0]}</FormLabel> |
50 | | - <CompositeNumberInput |
51 | | - value={color.r} |
52 | | - onChange={handleChangeR} |
53 | | - min={0} |
54 | | - max={255} |
55 | | - step={1} |
56 | | - w={numberInputWidth} |
57 | | - defaultValue={90} |
58 | | - /> |
59 | | - </FormControl> |
60 | | - <FormControl gap={0}> |
61 | | - <FormLabel>{t('common.green')[0]}</FormLabel> |
62 | | - <CompositeNumberInput |
63 | | - value={color.g} |
64 | | - onChange={handleChangeG} |
65 | | - min={0} |
66 | | - max={255} |
67 | | - step={1} |
68 | | - w={numberInputWidth} |
69 | | - defaultValue={90} |
70 | | - /> |
71 | | - </FormControl> |
72 | | - <FormControl gap={0}> |
73 | | - <FormLabel>{t('common.blue')[0]}</FormLabel> |
74 | | - <CompositeNumberInput |
75 | | - value={color.b} |
76 | | - onChange={handleChangeB} |
77 | | - min={0} |
78 | | - max={255} |
79 | | - step={1} |
80 | | - w={numberInputWidth} |
81 | | - defaultValue={255} |
82 | | - /> |
83 | | - </FormControl> |
84 | | - <FormControl gap={0}> |
85 | | - <FormLabel>{t('common.alpha')[0]}</FormLabel> |
86 | | - <CompositeNumberInput |
87 | | - value={color.a} |
88 | | - onChange={handleChangeA} |
89 | | - step={0.1} |
90 | | - min={0} |
91 | | - max={1} |
92 | | - w={numberInputWidth} |
93 | | - defaultValue={1} |
94 | | - /> |
95 | | - </FormControl> |
96 | | - </Flex> |
97 | | - )} |
| 77 | + {withNumberInput && |
| 78 | + (mode === 'rgb' ? ( |
| 79 | + <Flex gap={2} alignItems="end"> |
| 80 | + <Button |
| 81 | + size="xs" |
| 82 | + variant="ghost" |
| 83 | + px={3} |
| 84 | + minW="unset" |
| 85 | + h={10} |
| 86 | + whiteSpace="nowrap" |
| 87 | + onClick={onToggleMode} |
| 88 | + aria-label="Toggle RGB/HEX" |
| 89 | + > |
| 90 | + RGB |
| 91 | + </Button> |
| 92 | + <FormControl gap={0}> |
| 93 | + <FormLabel>{t('common.red')[0]}</FormLabel> |
| 94 | + <CompositeNumberInput |
| 95 | + value={color.r} |
| 96 | + onChange={handleChangeR} |
| 97 | + min={0} |
| 98 | + max={255} |
| 99 | + step={1} |
| 100 | + w={numberInputWidth} |
| 101 | + /> |
| 102 | + </FormControl> |
| 103 | + <FormControl gap={0}> |
| 104 | + <FormLabel>{t('common.green')[0]}</FormLabel> |
| 105 | + <CompositeNumberInput |
| 106 | + value={color.g} |
| 107 | + onChange={handleChangeG} |
| 108 | + min={0} |
| 109 | + max={255} |
| 110 | + step={1} |
| 111 | + w={numberInputWidth} |
| 112 | + /> |
| 113 | + </FormControl> |
| 114 | + <FormControl gap={0}> |
| 115 | + <FormLabel>{t('common.blue')[0]}</FormLabel> |
| 116 | + <CompositeNumberInput |
| 117 | + value={color.b} |
| 118 | + onChange={handleChangeB} |
| 119 | + min={0} |
| 120 | + max={255} |
| 121 | + step={1} |
| 122 | + w={numberInputWidth} |
| 123 | + /> |
| 124 | + </FormControl> |
| 125 | + <FormControl gap={0}> |
| 126 | + <FormLabel>{t('common.alpha')[0]}</FormLabel> |
| 127 | + <CompositeNumberInput |
| 128 | + value={color.a} |
| 129 | + onChange={handleChangeA} |
| 130 | + step={0.1} |
| 131 | + min={0} |
| 132 | + max={1} |
| 133 | + w={numberInputWidth} |
| 134 | + /> |
| 135 | + </FormControl> |
| 136 | + </Flex> |
| 137 | + ) : ( |
| 138 | + <Flex gap={2} alignItems="end"> |
| 139 | + <Button |
| 140 | + size="xs" |
| 141 | + variant="ghost" |
| 142 | + px={3} |
| 143 | + minW="unset" |
| 144 | + h={10} |
| 145 | + whiteSpace="nowrap" |
| 146 | + onClick={onToggleMode} |
| 147 | + aria-label="Toggle RGB/HEX" |
| 148 | + > |
| 149 | + HEX |
| 150 | + </Button> |
| 151 | + <FormControl gap={0}> |
| 152 | + <FormLabel>{t('common.hex', { defaultValue: 'Hex' })}</FormLabel> |
| 153 | + <Input value={hex} onChange={onChangeHex} placeholder="#RRGGBB or #RRGGBBAA" w="10rem" /> |
| 154 | + </FormControl> |
| 155 | + <FormControl gap={0}> |
| 156 | + <FormLabel>{t('common.alpha')[0]}</FormLabel> |
| 157 | + <CompositeNumberInput |
| 158 | + value={color.a} |
| 159 | + onChange={onChangeAlpha} |
| 160 | + step={0.01} |
| 161 | + min={0} |
| 162 | + max={1} |
| 163 | + w={numberInputWidth} |
| 164 | + /> |
| 165 | + </FormControl> |
| 166 | + </Flex> |
| 167 | + ))} |
98 | 168 | {withSwatches && ( |
99 | 169 | <Flex gap={2} justifyContent="space-between"> |
100 | 170 | {RGBA_COLOR_SWATCHES.map((color, i) => ( |
|
0 commit comments