Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { usePersistedTextAreaSize } from 'common/hooks/usePersistedTextareaSize'
import { negativePromptChanged, selectNegativePromptWithFallback } from 'features/controlLayers/store/paramsSlice';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { PromptResizeHandle } from 'features/parameters/components/Prompts/PromptResizeHandle';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
Expand All @@ -22,6 +23,8 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
trackHeight: true,
};

const NEGATIVE_PROMPT_MIN_HEIGHT = 28;

export const ParamNegativePrompt = memo(() => {
const dispatch = useAppDispatch();
const prompt = useAppSelector(selectNegativePromptWithFallback);
Expand Down Expand Up @@ -70,14 +73,16 @@ export const ParamNegativePrompt = memo(() => {
onChange={onChange}
onKeyDown={onKeyDown}
variant="darkFilled"
minH={28}
borderTopWidth={24} // This prevents the prompt from being hidden behind the header
paddingInlineEnd={10}
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
resize="none"
minH={NEGATIVE_PROMPT_MIN_HEIGHT}
fontFamily="mono"
fontSize="0.82rem"
sx={{ '&::-webkit-resizer': { display: 'none' } }}
/>
<PromptOverlayButtonWrapper>
<AddPromptTriggerButton isOpen={isOpen} onOpen={onOpen} />
Expand All @@ -90,6 +95,7 @@ export const ParamNegativePrompt = memo(() => {
label={`${t('parameters.negativePromptPlaceholder')} (${t('stylePresets.preview')})`}
/>
)}
<PromptResizeHandle textareaRef={textareaRef} minHeight={NEGATIVE_PROMPT_MIN_HEIGHT} />
</Box>
</PromptPopover>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/compone
import { NegativePromptToggleButton } from 'features/parameters/components/Core/NegativePromptToggleButton';
import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel';
import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper';
import { PromptResizeHandle } from 'features/parameters/components/Prompts/PromptResizeHandle';
import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt';
import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton';
import { PromptPopover } from 'features/prompt/PromptPopover';
Expand All @@ -35,6 +36,8 @@ const persistOptions: Parameters<typeof usePersistedTextAreaSize>[2] = {
initialHeight: 120,
};

const POSITIVE_PROMPT_MIN_HEIGHT = 32;

const usePromptHistory = () => {
const store = useAppStore();
const history = useAppSelector(selectPositivePromptHistory);
Expand Down Expand Up @@ -215,10 +218,11 @@ export const ParamPositivePrompt = memo(() => {
paddingInlineStart={3}
paddingTop={0}
paddingBottom={3}
resize="vertical"
minH={32}
resize="none"
minH={POSITIVE_PROMPT_MIN_HEIGHT}
fontFamily="mono"
fontSize="0.82rem"
sx={{ '&::-webkit-resizer': { display: 'none' } }}
/>
<PromptOverlayButtonWrapper>
<Flex flexDir="column" gap={2} justifyContent="flex-start" alignItems="center">
Expand All @@ -236,6 +240,7 @@ export const ParamPositivePrompt = memo(() => {
label={`${t('parameters.positivePromptPlaceholder')} (${t('stylePresets.preview')})`}
/>
)}
<PromptResizeHandle textareaRef={textareaRef} minHeight={POSITIVE_PROMPT_MIN_HEIGHT} />
</Box>
</PromptPopover>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Box } from '@invoke-ai/ui-library';
import {
memo,
type PointerEvent as ReactPointerEvent,
type RefObject,
useCallback,
useEffect,
useRef,
useState,
} from 'react';

type PromptResizeHandleProps = {
textareaRef: RefObject<HTMLTextAreaElement>;
minHeight: number;
};

const PROMPT_RESIZE_HANDLE_HEIGHT_PX = 8;

export const PromptResizeHandle = memo(({ textareaRef, minHeight }: PromptResizeHandleProps) => {
const activePointerIdRef = useRef<number | null>(null);
const startHeightRef = useRef(0);
const startYRef = useRef(0);
const previousCursorRef = useRef('');
const previousUserSelectRef = useRef('');
const [isResizing, setIsResizing] = useState(false);

const stopResize = useCallback(() => {
if (activePointerIdRef.current === null) {
return;
}

activePointerIdRef.current = null;
setIsResizing(false);
document.body.style.cursor = previousCursorRef.current;
document.body.style.userSelect = previousUserSelectRef.current;
}, []);

useEffect(() => stopResize, [stopResize]);

const onPointerDown = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
if (e.button !== 0) {
return;
}

const textarea = textareaRef.current;
if (!textarea) {
return;
}

activePointerIdRef.current = e.pointerId;
startYRef.current = e.clientY;
startHeightRef.current = textarea.offsetHeight;
previousCursorRef.current = document.body.style.cursor;
previousUserSelectRef.current = document.body.style.userSelect;

document.body.style.cursor = 'ns-resize';
document.body.style.userSelect = 'none';
e.currentTarget.setPointerCapture(e.pointerId);
setIsResizing(true);
e.preventDefault();
},
[textareaRef]
);

const onPointerMove = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
if (activePointerIdRef.current !== e.pointerId) {
return;
}

const textarea = textareaRef.current;
if (!textarea) {
return;
}

const nextHeight = Math.max(minHeight, startHeightRef.current + e.clientY - startYRef.current);
textarea.style.height = `${nextHeight}px`;
e.preventDefault();
},
[minHeight, textareaRef]
);

const onPointerUp = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
if (activePointerIdRef.current !== e.pointerId) {
return;
}

if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId);
}

stopResize();
},
[stopResize]
);

const onPointerCancel = useCallback(
(e: ReactPointerEvent<HTMLDivElement>) => {
if (activePointerIdRef.current !== e.pointerId) {
return;
}

stopResize();
},
[stopResize]
);

return (
<Box
aria-hidden
pos="absolute"
insetInlineStart={0}
insetInlineEnd={0}
insetBlockEnd={0}
h={`${PROMPT_RESIZE_HANDLE_HEIGHT_PX}px`}
borderBottomRadius="base"
bg={isResizing ? 'base.500' : 'base.700'}
cursor="ns-resize"
zIndex={1}
style={{ touchAction: 'none' }}
transitionProperty="background-color"
transitionDuration="normal"
_hover={{ bg: 'base.600' }}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
onLostPointerCapture={stopResize}
/>
);
});

PromptResizeHandle.displayName = 'PromptResizeHandle';
Loading