Files
luminance/src/hooks/slider.tsx
T
jay 5f6d0f43ee
Test and Build / test-and-build (push) Failing after 2m44s
Completed palette editor, ui overhaul.
2026-03-23 08:24:44 -04:00

240 lines
6.4 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from "react";
import type { CartesianSpace, Range, Setter } from "@/types";
import { Direction } from "@/types";
import {
chooseValueByDirection,
extractEventCoordinates,
isLeftMouseButton,
isTouchEvent,
minmax,
positionToValue,
valueToPosition,
} from "@/util";
import { useSmoothAnimation } from "./animation";
import { useScroll } from "./scroll";
if (typeof TouchEvent === "undefined") {
// @ts-expect-error - intentionally creating global
window.TouchEvent = window.MouseEvent;
}
function extractEventCoordinateByDirection(
event: MouseEvent | TouchEvent,
direction: Direction,
): number {
const { clientX, clientY } = extractEventCoordinates(event);
return chooseValueByDirection(direction, clientX, clientY);
}
export function useSlider({
direction,
origin,
dimensions,
valueRange,
value,
setValue,
invert = false,
}: {
direction: Direction;
origin: CartesianSpace;
dimensions: CartesianSpace;
valueRange: Range;
value: number;
setValue: Setter<number>;
invert?: boolean;
}) {
const [isDragging, setIsDragging] = useState(false);
const sliderRef = useRef<HTMLDivElement>(null);
// Slider UI refs
const directionRef = useRef(direction);
const originRef = useRef(origin);
const dimensionsRef = useRef(dimensions);
// Slider value refs
const setValueRef = useRef(setValue);
const valueRangeRef = useRef(valueRange);
const maxPosition = useRef(0);
// Internal position management
const position = valueToPosition(
value,
chooseValueByDirection(direction, dimensions.x, dimensions.y),
valueRange,
);
const positionRef = useRef(position);
// Hooks
const smoothAnimation = useSmoothAnimation();
useEffect(() => {
directionRef.current = direction;
originRef.current = origin;
dimensionsRef.current = dimensions;
maxPosition.current = chooseValueByDirection(
direction,
dimensions.x,
dimensions.y,
);
positionRef.current = valueToPosition(
value,
maxPosition.current,
valueRangeRef.current,
);
}, [direction, origin, dimensions, value]);
useEffect(() => {
valueRangeRef.current = valueRange;
}, [valueRange, valueRangeRef]);
useEffect(() => {
setValueRef.current = setValue;
}, [setValue]);
useEffect(() => {
positionRef.current = position;
}, [position]);
// Setup drag handlers
const calculatePosition = useCallback(
(event: MouseEvent | TouchEvent) => {
const dir = directionRef.current;
const orig = originRef.current;
const dims = dimensionsRef.current;
const clientCoord = extractEventCoordinateByDirection(event, dir);
const newPosition = minmax(
clientCoord - chooseValueByDirection(dir, orig.x, orig.y),
0,
chooseValueByDirection(dir, dims.x, dims.y),
);
let newValue = positionToValue(
newPosition,
maxPosition.current,
valueRangeRef.current,
);
if (invert) {
newValue = valueRangeRef.current.max - newValue;
}
setValueRef.current(newValue);
},
[invert],
);
const handleMove = useCallback(
(event: MouseEvent | TouchEvent) => {
event.preventDefault();
smoothAnimation(() => calculatePosition(event));
},
[calculatePosition, smoothAnimation],
);
const handleEnd = useCallback(() => {
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleEnd);
document.removeEventListener("touchmove", handleMove);
document.removeEventListener("touchend", handleEnd);
document.removeEventListener("touchcancel", handleEnd);
setIsDragging(false);
}, [handleMove]);
const handleStart = useCallback(
(event: MouseEvent | TouchEvent) => {
if (!isTouchEvent(event) && !isLeftMouseButton(event.buttons)) {
return;
}
event.preventDefault();
calculatePosition(event);
setIsDragging(true);
document.addEventListener("mousemove", handleMove);
document.addEventListener("mouseup", handleEnd, { passive: true });
document.addEventListener("touchmove", handleMove);
document.addEventListener("touchend", handleEnd, { passive: true });
document.addEventListener("touchcancel", handleEnd, { passive: true });
},
[calculatePosition, handleMove, handleEnd],
);
// Setup scroll handlers
const handleScrollUp = useCallback(() => {
const dir = directionRef.current;
const dims = dimensionsRef.current;
const inc = chooseValueByDirection(dir, 1, -1);
const newPosition = minmax(
positionRef.current + inc,
0,
chooseValueByDirection(dir, dims.x, dims.y),
);
const newValue = positionToValue(
newPosition,
maxPosition.current,
valueRangeRef.current,
);
setValueRef.current(newValue);
}, []);
const handleScrollDown = useCallback(() => {
const dir = directionRef.current;
const dims = dimensionsRef.current;
const inc = chooseValueByDirection(dir, -1, 1);
const newPosition = minmax(
positionRef.current + inc,
0,
chooseValueByDirection(dir, dims.x, dims.y),
);
const newValue = positionToValue(
newPosition,
maxPosition.current,
valueRangeRef.current,
);
setValueRef.current(newValue);
}, []);
const { addScrollListener, removeScrollListener } = useScroll({
targetRef: sliderRef,
onScrollUp: handleScrollUp,
onScrollDown: handleScrollDown,
});
// Set up entry listeners
useEffect(() => {
const currentRef = sliderRef.current;
if (currentRef) {
addScrollListener();
currentRef.addEventListener("mousedown", handleStart);
currentRef.addEventListener("touchstart", handleStart);
}
return () => {
if (currentRef) {
removeScrollListener();
currentRef.removeEventListener("mousedown", handleStart);
currentRef.removeEventListener("touchstart", handleStart);
}
document.removeEventListener("mousemove", handleMove);
document.removeEventListener("mouseup", handleEnd);
document.removeEventListener("touchmove", handleMove);
document.removeEventListener("touchend", handleEnd);
document.removeEventListener("touchcancel", handleEnd);
};
}, [
addScrollListener,
removeScrollListener,
handleStart,
handleMove,
handleEnd,
]);
return { sliderRef, isDragging };
}