Skip to content

Commit c6a9847

Browse files
feat(ui): Color Picker V2 (#8585)
* pinned colorpicker * hex options * remove unused consts --------- Co-authored-by: Lincoln Stein <lincoln.stein@gmail.com>
1 parent a2e109b commit c6a9847

File tree

5 files changed

+278
-59
lines changed

5 files changed

+278
-59
lines changed

invokeai/frontend/web/src/common/components/ColorPicker/RgbaColorPicker.tsx

Lines changed: 126 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
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';
33
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';
77
import { RgbaColorPicker as ColorfulRgbaColorPicker } from 'react-colorful';
88
import type { RgbaColor } from 'react-colorful/dist/types';
99
import { useTranslation } from 'react-i18next';
@@ -40,61 +40,131 @@ const RgbaColorPicker = (props: Props) => {
4040
const handleChangeG = useCallback((g: number) => onChange({ ...color, g }), [color, onChange]);
4141
const handleChangeB = useCallback((b: number) => onChange({ ...color, b }), [color, onChange]);
4242
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+
);
4374
return (
4475
<Flex sx={sx}>
4576
<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+
))}
98168
{withSwatches && (
99169
<Flex gap={2} justifyContent="space-between">
100170
{RGBA_COLOR_SWATCHES.map((color, i) => (
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Flex, IconButton, Text } from '@invoke-ai/ui-library';
2+
import { createSelector } from '@reduxjs/toolkit';
3+
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
4+
import RgbaColorPicker from 'common/components/ColorPicker/RgbaColorPicker';
5+
import {
6+
selectCanvasSettingsSlice,
7+
settingsActiveColorToggled,
8+
settingsBgColorChanged,
9+
settingsFgColorChanged,
10+
settingsFillColorPickerPinnedSet,
11+
} from 'features/controlLayers/store/canvasSettingsSlice';
12+
import type { RgbaColor } from 'features/controlLayers/store/types';
13+
import { memo, useCallback, useMemo } from 'react';
14+
import { useTranslation } from 'react-i18next';
15+
import { PiArrowsLeftRightBold, PiPushPinSlashBold } from 'react-icons/pi';
16+
17+
const selectActiveColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.activeColor);
18+
const selectBgColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.bgColor);
19+
const selectFgColor = createSelector(selectCanvasSettingsSlice, (settings) => settings.fgColor);
20+
const selectPinned = createSelector(selectCanvasSettingsSlice, (settings) => settings.fillColorPickerPinned);
21+
22+
export const PinnedFillColorPickerOverlay = memo(() => {
23+
const { t } = useTranslation();
24+
const isPinned = useAppSelector(selectPinned);
25+
const activeColorType = useAppSelector(selectActiveColor);
26+
const bgColor = useAppSelector(selectBgColor);
27+
const fgColor = useAppSelector(selectFgColor);
28+
const dispatch = useAppDispatch();
29+
30+
const activeColor = useMemo(
31+
() => (activeColorType === 'bgColor' ? bgColor : fgColor),
32+
[activeColorType, bgColor, fgColor]
33+
);
34+
35+
const onColorChange = useCallback(
36+
(color: RgbaColor) => {
37+
if (activeColorType === 'bgColor') {
38+
dispatch(settingsBgColorChanged(color));
39+
} else {
40+
dispatch(settingsFgColorChanged(color));
41+
}
42+
},
43+
[activeColorType, dispatch]
44+
);
45+
46+
const onUnpin = useCallback(() => dispatch(settingsFillColorPickerPinnedSet(false)), [dispatch]);
47+
const onToggleActive = useCallback(() => dispatch(settingsActiveColorToggled()), [dispatch]);
48+
49+
if (!isPinned) {
50+
return null;
51+
}
52+
53+
return (
54+
<Flex pointerEvents="auto" direction="column" gap={2}>
55+
<Flex
56+
direction="column"
57+
p={3}
58+
bg="base.900"
59+
borderColor="base.700"
60+
borderWidth="1px"
61+
borderStyle="solid"
62+
shadow="dark-lg"
63+
borderRadius="base"
64+
minW={88}
65+
>
66+
<Flex justifyContent="space-between" alignItems="center" mb={2} gap={2}>
67+
<Text fontWeight="semibold" color="base.300">
68+
{t('controlLayers.fill.fillColor')}
69+
</Text>
70+
<Flex gap={1}>
71+
<IconButton
72+
aria-label={t('controlLayers.fill.switchColors', { defaultValue: 'Switch FG/BG (X)' })}
73+
tooltip={t('controlLayers.fill.switchColors', { defaultValue: 'Switch FG/BG (X)' })}
74+
size="sm"
75+
variant="ghost"
76+
onClick={onToggleActive}
77+
icon={<PiArrowsLeftRightBold />}
78+
/>
79+
<IconButton
80+
aria-label={t('common.unpin', { defaultValue: 'Unpin' })}
81+
tooltip={t('common.unpin', { defaultValue: 'Unpin' })}
82+
size="sm"
83+
variant="solid"
84+
onClick={onUnpin}
85+
icon={<PiPushPinSlashBold />}
86+
/>
87+
</Flex>
88+
</Flex>
89+
<RgbaColorPicker color={activeColor} onChange={onColorChange} withNumberInput withSwatches />
90+
</Flex>
91+
</Flex>
92+
);
93+
});
94+
95+
PinnedFillColorPickerOverlay.displayName = 'PinnedFillColorPickerOverlay';

0 commit comments

Comments
 (0)