memlnaut-nisps/manifold/src/console/DualRange.tsx
2026-07-25 15:05:32 +02:00

102 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* DualRange — one bounded, dual-thumb control for an output's routing range.
* The same control is used by the main-view output editor and the Outputs
* drawer so min/max edits have identical interaction and clamping semantics.
*/
import { useRef } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react';
export interface DualRangeProps {
min: number;
max: number;
onMin: (value: number) => void;
onMax: (value: number) => void;
}
export function DualRange({ min, max, onMin, onMax }: DualRangeProps) {
const track = useRef<HTMLDivElement>(null);
const drag = useRef<{ which: 'min' | 'max' | null }>({ which: null });
const valueAt = (clientX: number) => {
const el = track.current;
if (!el) return 0;
const rect = el.getBoundingClientRect();
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
};
const apply = (value: number) => {
if (drag.current.which === 'min') onMin(Math.min(value, max));
else if (drag.current.which === 'max') onMax(Math.max(value, min));
};
const down = (event: ReactPointerEvent<HTMLDivElement>) => {
event.currentTarget.setPointerCapture?.(event.pointerId);
const value = valueAt(event.clientX);
drag.current.which = Math.abs(value - min) <= Math.abs(value - max) ? 'min' : 'max';
apply(value);
};
const move = (event: ReactPointerEvent<HTMLDivElement>) => {
if (drag.current.which) apply(valueAt(event.clientX));
};
const up = () => {
drag.current.which = null;
};
return (
<div
ref={track}
onPointerDown={down}
onPointerMove={move}
onPointerUp={up}
onPointerCancel={up}
title={`range ${min.toFixed(2)}${max.toFixed(2)}`}
style={{
position: 'relative',
height: 16,
flex: 1,
minWidth: 60,
background: 'var(--bg-1)',
border: '1px solid var(--line)',
borderRadius: 'var(--r-pill)',
cursor: 'ew-resize',
touchAction: 'none',
}}
>
<div
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: `${min * 100}%`,
right: `${(1 - max) * 100}%`,
background: 'linear-gradient(90deg, #4488ff, var(--accent))',
opacity: 0.4,
borderRadius: 'var(--r-pill)',
}}
/>
<Thumb pct={min} color="#4488ff" />
<Thumb pct={max} color="var(--accent)" />
</div>
);
}
function Thumb({ pct, color }: { pct: number; color: string }) {
return (
<div
style={{
position: 'absolute',
top: '50%',
left: `${pct * 100}%`,
width: 10,
height: 10,
marginLeft: -5,
marginTop: -5,
borderRadius: '50%',
background: color,
boxShadow: `0 0 6px ${color}`,
}}
/>
);
}