Skip to content

Commit 1b50c1a

Browse files
Feat(UI): Replace prompt window resize handle with bottom edge drag handle. (#8975)
* feat(ui): replace prompt window resize handle with bottom-edge drag handle * Fix: removed unused export --------- Co-authored-by: Josh Corbett <joshwcorbett@icloud.com>
1 parent acd4157 commit 1b50c1a

3 files changed

Lines changed: 149 additions & 3 deletions

File tree

invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize'
44
import { negativePromptChanged, selectNegativePromptWithFallback } from 'features/controlLayers/store/paramsSlice';
55
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
66
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
7+
import { PromptResizeHandle } from 'features/parameters/components/Prompts/PromptResizeHandle';
78
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
89
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
910
import { PromptPopover } from 'features/prompt/PromptPopover';
@@ -22,6 +23,8 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
2223
trackHeight: true,
2324
};
2425

26+
const NEGATIVE_PROMPT_MIN_HEIGHT = 28;
27+
2528
export const ParamNegativePrompt = memo(() => {
2629
const dispatch = useAppDispatch();
2730
const prompt = useAppSelector(selectNegativePromptWithFallback);
@@ -70,14 +73,16 @@ export const ParamNegativePrompt = memo(() => {
7073
onChange={onChange}
7174
onKeyDown={onKeyDown}
7275
variant="darkFilled"
73-
minH={28}
7476
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
7577
paddingInlineEnd={10}
7678
paddingInlineStart={3}
7779
paddingTop={0}
7880
paddingBottom={3}
81+
resize="none"
82+
minH={NEGATIVE_PROMPT_MIN_HEIGHT}
7983
fontFamily="mono"
8084
fontSize="0.82rem"
85+
sx={{ '&::-webkit-resizer': { display: 'none' } }}
8186
/>
8287
<PromptOverlayButtonWrapper>
8388
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
@@ -90,6 +95,7 @@ export const ParamNegativePrompt = memo(() => {
9095
label={`${t('parameters.negativePromptPlaceholder')} (${t('stylePresets.preview')})`}
9196
/>
9297
)}
98+
<PromptResizeHandle textareaRef={textareaRef} minHeight={NEGATIVE_PROMPT_MIN_HEIGHT} />
9399
</Box>
94100
</PromptPopover>
95101
);

invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/compone
1111
import { NegativePromptToggleButton } from 'features/parameters/components/Core/NegativePromptToggleButton';
1212
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
1313
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
14+
import { PromptResizeHandle } from 'features/parameters/components/Prompts/PromptResizeHandle';
1415
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
1516
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
1617
import { PromptPopover } from 'features/prompt/PromptPopover';
@@ -35,6 +36,8 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
3536
initialHeight: 120,
3637
};
3738

39+
const POSITIVE_PROMPT_MIN_HEIGHT = 32;
40+
3841
const usePromptHistory = () => {
3942
const store = useAppStore();
4043
const history = useAppSelector(selectPositivePromptHistory);
@@ -215,10 +218,11 @@ export const ParamPositivePrompt = memo(() => {
215218
paddingInlineStart={3}
216219
paddingTop={0}
217220
paddingBottom={3}
218-
resize="vertical"
219-
minH={32}
221+
resize="none"
222+
minH={POSITIVE_PROMPT_MIN_HEIGHT}
220223
fontFamily="mono"
221224
fontSize="0.82rem"
225+
sx={{ '&::-webkit-resizer': { display: 'none' } }}
222226
/>
223227
<PromptOverlayButtonWrapper>
224228
<Flex flexDir="column" gap={2} justifyContent="flex-start" alignItems="center">
@@ -236,6 +240,7 @@ export const ParamPositivePrompt = memo(() => {
236240
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
237241
/>
238242
)}
243+
<PromptResizeHandle textareaRef={textareaRef} minHeight={POSITIVE_PROMPT_MIN_HEIGHT} />
239244
</Box>
240245
</PromptPopover>
241246
</Box>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { Box } from '@invoke-ai/ui-library';
2+
import {
3+
memo,
4+
type PointerEvent as ReactPointerEvent,
5+
type RefObject,
6+
useCallback,
7+
useEffect,
8+
useRef,
9+
useState,
10+
} from 'react';
11+
12+
type PromptResizeHandleProps = {
13+
textareaRef: RefObject<HTMLTextAreaElement>;
14+
minHeight: number;
15+
};
16+
17+
const PROMPT_RESIZE_HANDLE_HEIGHT_PX = 8;
18+
19+
export const PromptResizeHandle = memo(({ textareaRef, minHeight }: PromptResizeHandleProps) => {
20+
const activePointerIdRef = useRef<number | null>(null);
21+
const startHeightRef = useRef(0);
22+
const startYRef = useRef(0);
23+
const previousCursorRef = useRef('');
24+
const previousUserSelectRef = useRef('');
25+
const [isResizing, setIsResizing] = useState(false);
26+
27+
const stopResize = useCallback(() => {
28+
if (activePointerIdRef.current === null) {
29+
return;
30+
}
31+
32+
activePointerIdRef.current = null;
33+
setIsResizing(false);
34+
document.body.style.cursor = previousCursorRef.current;
35+
document.body.style.userSelect = previousUserSelectRef.current;
36+
}, []);
37+
38+
useEffect(() => stopResize, [stopResize]);
39+
40+
const onPointerDown = useCallback(
41+
(e: ReactPointerEvent<HTMLDivElement>) => {
42+
if (e.button !== 0) {
43+
return;
44+
}
45+
46+
const textarea = textareaRef.current;
47+
if (!textarea) {
48+
return;
49+
}
50+
51+
activePointerIdRef.current = e.pointerId;
52+
startYRef.current = e.clientY;
53+
startHeightRef.current = textarea.offsetHeight;
54+
previousCursorRef.current = document.body.style.cursor;
55+
previousUserSelectRef.current = document.body.style.userSelect;
56+
57+
document.body.style.cursor = 'ns-resize';
58+
document.body.style.userSelect = 'none';
59+
e.currentTarget.setPointerCapture(e.pointerId);
60+
setIsResizing(true);
61+
e.preventDefault();
62+
},
63+
[textareaRef]
64+
);
65+
66+
const onPointerMove = useCallback(
67+
(e: ReactPointerEvent<HTMLDivElement>) => {
68+
if (activePointerIdRef.current !== e.pointerId) {
69+
return;
70+
}
71+
72+
const textarea = textareaRef.current;
73+
if (!textarea) {
74+
return;
75+
}
76+
77+
const nextHeight = Math.max(minHeight, startHeightRef.current + e.clientY - startYRef.current);
78+
textarea.style.height = `${nextHeight}px`;
79+
e.preventDefault();
80+
},
81+
[minHeight, textareaRef]
82+
);
83+
84+
const onPointerUp = useCallback(
85+
(e: ReactPointerEvent<HTMLDivElement>) => {
86+
if (activePointerIdRef.current !== e.pointerId) {
87+
return;
88+
}
89+
90+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
91+
e.currentTarget.releasePointerCapture(e.pointerId);
92+
}
93+
94+
stopResize();
95+
},
96+
[stopResize]
97+
);
98+
99+
const onPointerCancel = useCallback(
100+
(e: ReactPointerEvent<HTMLDivElement>) => {
101+
if (activePointerIdRef.current !== e.pointerId) {
102+
return;
103+
}
104+
105+
stopResize();
106+
},
107+
[stopResize]
108+
);
109+
110+
return (
111+
<Box
112+
aria-hidden
113+
pos="absolute"
114+
insetInlineStart={0}
115+
insetInlineEnd={0}
116+
insetBlockEnd={0}
117+
h={`${PROMPT_RESIZE_HANDLE_HEIGHT_PX}px`}
118+
borderBottomRadius="base"
119+
bg={isResizing ? 'base.500' : 'base.700'}
120+
cursor="ns-resize"
121+
zIndex={1}
122+
style={{ touchAction: 'none' }}
123+
transitionProperty="background-color"
124+
transitionDuration="normal"
125+
_hover={{ bg: 'base.600' }}
126+
onPointerDown={onPointerDown}
127+
onPointerMove={onPointerMove}
128+
onPointerUp={onPointerUp}
129+
onPointerCancel={onPointerCancel}
130+
onLostPointerCapture={stopResize}
131+
/>
132+
);
133+
});
134+
135+
PromptResizeHandle.displayName = 'PromptResizeHandle';

0 commit comments

Comments
 (0)