Skip to content
Merged
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 @@ -23,7 +23,7 @@ import {
settingsEraserWidthChanged,
} from 'features/controlLayers/store/canvasSettingsSlice';
import { useRegisteredHotkeys } from 'features/system/components/HotkeysModal/useHotkeyData';
import type { KeyboardEvent } from 'react';
import type { FocusEvent, KeyboardEvent, PointerEvent } from 'react';
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { PiCaretDownBold } from 'react-icons/pi';

Expand Down Expand Up @@ -71,17 +71,40 @@ const marks = [
const sliderDefaultValue = mapRawValueToSliderValue(50);

const SLIDER_VS_DROPDOWN_CONTAINER_WIDTH_THRESHOLD = 280;
const DEFAULT_TOOL_WIDTH = 50;
const parseInputValue = (value: string) => Number.parseFloat(value);
const getInputValueFromEvent = (
event?: Pick<FocusEvent<HTMLElement> | KeyboardEvent<HTMLElement>, 'target' | 'currentTarget'>
) => {
const target = event?.target as HTMLInputElement | null;
if (target?.tagName === 'INPUT') {
return { input: target, parsed: parseInputValue(target.value) };
}
const currentTarget = event?.currentTarget as HTMLElement | null;
const input = currentTarget?.querySelector('input') ?? null;
return { input, parsed: input ? parseInputValue(input.value) : NaN };
};

interface ToolWidthPickerComponentProps {
localValue: number;
onChangeSlider: (value: number) => void;
onChangeInput: (value: number) => void;
onBlur: () => void;
onBlur: (event?: FocusEvent<HTMLElement>) => void;
onKeyDown: (value: KeyboardEvent<HTMLInputElement>) => void;
onPointerDownCapture: (value: PointerEvent<HTMLDivElement>) => void;
onPointerUpCapture: (value: PointerEvent<HTMLDivElement>) => void;
}

const DropDownToolWidthPickerComponent = memo(
({ localValue, onChangeSlider, onChangeInput, onKeyDown, onBlur }: ToolWidthPickerComponentProps) => {
({
localValue,
onChangeSlider,
onChangeInput,
onKeyDown,
onPointerDownCapture,
onPointerUpCapture,
onBlur,
}: ToolWidthPickerComponentProps) => {
const onChangeNumberInput = useCallback(
(valueAsString: string, valueAsNumber: number) => {
onChangeInput(valueAsNumber);
Expand All @@ -106,6 +129,8 @@ const DropDownToolWidthPickerComponent = memo(
format={formatPx}
defaultValue={50}
onKeyDown={onKeyDown}
onPointerDownCapture={onPointerDownCapture}
onPointerUpCapture={onPointerUpCapture}
clampValueOnBlur={false}
>
<NumberInputField _focusVisible={{ zIndex: 0 }} title="" paddingInlineEnd={7} />
Expand Down Expand Up @@ -147,7 +172,15 @@ const DropDownToolWidthPickerComponent = memo(
DropDownToolWidthPickerComponent.displayName = 'DropDownToolWidthPickerComponent';

const SliderToolWidthPickerComponent = memo(
({ localValue, onChangeSlider, onChangeInput, onKeyDown, onBlur }: ToolWidthPickerComponentProps) => {
({
localValue,
onChangeSlider,
onChangeInput,
onKeyDown,
onPointerDownCapture,
onPointerUpCapture,
onBlur,
}: ToolWidthPickerComponentProps) => {
return (
<Flex w={SLIDER_VS_DROPDOWN_CONTAINER_WIDTH_THRESHOLD} gap={4}>
<CompositeSlider
Expand All @@ -171,6 +204,8 @@ const SliderToolWidthPickerComponent = memo(
onChange={onChangeInput}
onBlur={onBlur}
onKeyDown={onKeyDown}
onPointerDownCapture={onPointerDownCapture}
onPointerUpCapture={onPointerUpCapture}
format={formatPx}
defaultValue={50}
/>
Expand Down Expand Up @@ -204,6 +239,8 @@ export const ToolWidthPicker = memo(() => {
}, [isBrushSelected, isEraserSelected, brushWidth, eraserWidth]);
const [localValue, setLocalValue] = useState(width);
const [componentType, setComponentType] = useState<'slider' | 'dropdown' | null>(null);
const isTypingRef = useRef(false);
const inputPollRef = useRef<number | null>(null);

useEffect(() => {
const el = ref.current;
Expand Down Expand Up @@ -244,6 +281,65 @@ export const ToolWidthPicker = memo(() => {
[onValueChange]
);

const syncFromInputElement = useCallback(
(input: HTMLInputElement | null) => {
if (!input) {
return;
}
const parsed = parseInputValue(input.value);
if (Number.isNaN(parsed)) {
return;
}
setLocalValue(parsed);
onChange(parsed);
},
[onChange]
);

const stopPollingInput = useCallback(() => {
if (inputPollRef.current !== null) {
window.clearInterval(inputPollRef.current);
inputPollRef.current = null;
}
}, []);

const startPollingInput = useCallback(
(container: HTMLElement | null) => {
stopPollingInput();
if (!container) {
return;
}
inputPollRef.current = window.setInterval(() => {
const input = container.querySelector('input');
if (!input) {
return;
}
const parsed = parseInputValue(input.value);
if (Number.isNaN(parsed)) {
return;
}
setLocalValue(parsed);
if (!isTypingRef.current) {
onChange(parsed);
}
}, 50);
},
[onChange, stopPollingInput]
);

const commitValue = useCallback(
(value: number) => {
if (isNaN(Number(value))) {
onChange(DEFAULT_TOOL_WIDTH);
setLocalValue(DEFAULT_TOOL_WIDTH);
} else {
onChange(value);
setLocalValue(value);
}
},
[onChange]
);

const increment = useCallback(() => {
let newWidth = Math.round(width * 1.15);
if (newWidth === width) {
Expand All @@ -267,32 +363,75 @@ export const ToolWidthPicker = memo(() => {
[onChange]
);

const onChangeInput = useCallback((value: number) => {
setLocalValue(value);
}, []);
const onChangeInput = useCallback(
(value: number) => {
setLocalValue(value);
if (!isNaN(value) && !isTypingRef.current) {
onChange(value);
}
},
[onChange]
);

const onBlur = useCallback(() => {
if (isNaN(Number(localValue))) {
onChange(50);
setLocalValue(50);
} else {
onChange(localValue);
}
}, [localValue, onChange]);
const onBlur = useCallback(
(event?: FocusEvent<HTMLElement>) => {
const { parsed } = getInputValueFromEvent(event);
commitValue(Number.isNaN(parsed) ? localValue : parsed);
isTypingRef.current = false;
},
[commitValue, localValue]
);

const onKeyDown = useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
onBlur();
const { parsed } = getInputValueFromEvent(e);
commitValue(Number.isNaN(parsed) ? localValue : parsed);
isTypingRef.current = false;
return;
}
if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
isTypingRef.current = false;
const { input } = getInputValueFromEvent(e);
window.requestAnimationFrame(() => {
syncFromInputElement(input);
});
return;
}
if (e.key === 'Backspace' || e.key === 'Delete' || e.key.length === 1) {
isTypingRef.current = true;
}
},
[commitValue, localValue, syncFromInputElement]
);

const onPointerDownCapture = useCallback(
(_e: PointerEvent<HTMLDivElement>) => {
isTypingRef.current = false;
const target = _e.target as HTMLElement | null;
if (target && target.tagName !== 'INPUT') {
startPollingInput(_e.currentTarget);
} else {
stopPollingInput();
}
},
[onBlur]
[startPollingInput, stopPollingInput]
);

const onPointerUpCapture = useCallback(() => {
stopPollingInput();
}, [stopPollingInput]);

useEffect(() => {
setLocalValue(width);
}, [width]);

useEffect(() => {
return () => {
stopPollingInput();
};
}, [stopPollingInput]);

useRegisteredHotkeys({
id: 'decrementToolWidth',
category: 'canvas',
Expand All @@ -317,6 +456,8 @@ export const ToolWidthPicker = memo(() => {
onChangeInput={onChangeInput}
onBlur={onBlur}
onKeyDown={onKeyDown}
onPointerDownCapture={onPointerDownCapture}
onPointerUpCapture={onPointerUpCapture}
/>
)}
{componentType === 'dropdown' && (
Expand All @@ -326,6 +467,8 @@ export const ToolWidthPicker = memo(() => {
onChangeInput={onChangeInput}
onBlur={onBlur}
onKeyDown={onKeyDown}
onPointerDownCapture={onPointerDownCapture}
onPointerUpCapture={onPointerUpCapture}
/>
)}
</Flex>
Expand Down