From faa019d52bd2549c502b217fa1fb87e006dc6cad Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Sun, 13 Sep 2026 10:50:43 +0200 Subject: [PATCH] fix: matrix choice selector --- src/components/ChoiceSelect/ChoiceSelect.tsx | 8 +- .../ChoiceStepper/ChoiceStepper.css | 1 + .../ChoiceStepper/ChoiceStepper.tsx | 265 +++++++++++++++--- .../CompressorSection/CompressorSection.tsx | 36 +-- .../LimiterSection/LimiterSection.tsx | 7 +- .../Matrixsection/Matrixsection.css | 9 +- 6 files changed, 254 insertions(+), 72 deletions(-) diff --git a/src/components/ChoiceSelect/ChoiceSelect.tsx b/src/components/ChoiceSelect/ChoiceSelect.tsx index 18f6b1a..4c2ca43 100644 --- a/src/components/ChoiceSelect/ChoiceSelect.tsx +++ b/src/components/ChoiceSelect/ChoiceSelect.tsx @@ -28,17 +28,13 @@ function ChoiceSelect({ if (currentIndex === -1 || options.length === 0) return; - const nextIndex = - (currentIndex + direction + options.length) % options.length; + const nextIndex = (currentIndex + direction + options.length) % options.length; onChange(options[nextIndex]); }; return ( -
+
({ e.preventDefault(); if (e.deltaY < 0 && canUp) { - step(1); + stepWheel(1); } else if (e.deltaY > 0 && canDown) { - step(-1); + stepWheel(-1); } }} onKeyDown={(e) => { - if (e.key === 'Enter') e.currentTarget.blur(); - else if (e.key === 'ArrowUp') { + if (e.key === 'Enter') { + e.currentTarget.blur(); + } else if (e.key === 'ArrowUp') { e.preventDefault(); - step(1); + interacting.current = true; + stepLocal(1); } else if (e.key === 'ArrowDown') { e.preventDefault(); - step(-1); + interacting.current = true; + stepLocal(-1); } }} + onKeyUp={(e) => { + if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; + void commit(localValue); + }} /> step(1)} - onDown={() => step(-1)} + onUp={() => stepAndCommit(1)} + onDown={() => stepAndCommit(-1)} canUp={canUp} canDown={canDown} disabled={disabled} @@ -137,7 +232,7 @@ type ChoiceStepperRangeProps = { max: number; step?: number; formatLabel: (value: number) => string; - onChange: (value: number) => void; + onChange: (value: number) => CommitResult; id?: string; disabled?: boolean; className?: string; @@ -154,30 +249,100 @@ function ChoiceStepperRange({ disabled, className, }: ChoiceStepperRangeProps) { + const [localValue, setLocalValue] = useState(value); const [text, setText] = useState(formatLabel(value)); - useEffect(() => setText(formatLabel(value)), [value, formatLabel]); + + const interacting = useRef(false); + const pendingValue = useRef(null); + const wheelTimeout = useRef | null>(null); + const fallbackTimeout = useRef | null>(null); + + useEffect(() => { + if (interacting.current) { + if (pendingValue.current !== null && Object.is(value, pendingValue.current)) { + interacting.current = false; + pendingValue.current = null; + if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current); + } else { + return; + } + } + setLocalValue(value); + setText(formatLabel(value)); + }, [value, formatLabel]); + + useEffect(() => { + return () => { + if (wheelTimeout.current) clearTimeout(wheelTimeout.current); + if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current); + }; + }, []); const clamp = (n: number) => Math.min(max, Math.max(min, n)); - const canUp = value < max; - const canDown = value > min; + const canUp = localValue < max; + const canDown = localValue > min; - const step = (direction: 1 | -1) => { - const next = clamp(value + direction * stepSize); - if (next !== value) onChange(next); + const stepLocal = (direction: 1 | -1): number | null => { + const next = clamp(localValue + direction * stepSize); + if (next === localValue) return null; + setLocalValue(next); + setText(formatLabel(next)); + return next; + }; + + async function commit(next: number) { + interacting.current = true; + pendingValue.current = next; + + const result = await onChange(next); + + if (result === false) { + interacting.current = false; + pendingValue.current = null; + if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current); + setLocalValue(value); + setText(formatLabel(value)); + return; + } + + if (fallbackTimeout.current) clearTimeout(fallbackTimeout.current); + fallbackTimeout.current = setTimeout(() => { + interacting.current = false; + pendingValue.current = null; + }, PENDING_FALLBACK_MS); + } + + const stepAndCommit = (direction: 1 | -1) => { + const next = stepLocal(direction); + if (next !== null) void commit(next); + }; + + const stepWheel = (direction: 1 | -1) => { + const next = stepLocal(direction); + if (next === null) return; + + interacting.current = true; + if (wheelTimeout.current) clearTimeout(wheelTimeout.current); + wheelTimeout.current = setTimeout(() => { + wheelTimeout.current = null; + void commit(next); + }, WHEEL_COMMIT_DELAY); }; const commitText = () => { const parsed = Number(text); - if (Number.isFinite(parsed)) onChange(clamp(parsed)); - else setText(formatLabel(value)); + if (Number.isFinite(parsed)) { + const next = clamp(parsed); + setLocalValue(next); + void commit(next); + } else { + setText(formatLabel(value)); + } }; return ( -
+
0 && canDown) { - step(-1); + stepWheel(-1); } }} onKeyDown={(e) => { @@ -199,16 +364,26 @@ function ChoiceStepperRange({ e.currentTarget.blur(); } else if (e.key === 'ArrowUp') { e.preventDefault(); - if (canUp) step(1); + if (canUp) { + interacting.current = true; + stepLocal(1); + } } else if (e.key === 'ArrowDown') { e.preventDefault(); - if (canDown) step(-1); + if (canDown) { + interacting.current = true; + stepLocal(-1); + } } }} + onKeyUp={(e) => { + if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return; + void commit(localValue); + }} /> step(1)} - onDown={() => step(-1)} + onUp={() => stepAndCommit(1)} + onDown={() => stepAndCommit(-1)} canUp={canUp} canDown={canDown} disabled={disabled} diff --git a/src/features/dsp408/components/CompressorSection/CompressorSection.tsx b/src/features/dsp408/components/CompressorSection/CompressorSection.tsx index bb8bec7..f49c1f8 100644 --- a/src/features/dsp408/components/CompressorSection/CompressorSection.tsx +++ b/src/features/dsp408/components/CompressorSection/CompressorSection.tsx @@ -81,29 +81,29 @@ function CompressorPanel({
RATIO_LABEL[v]} - onChange={(value) => { - onLiveValue(channel, 'ratio', value); - commit({ ratio: value }); - }} -/> + className="compressor-panel__stepper compressor-panel__stepper--centered" + value={comp.ratio} + values={RATIO_ORDER} + formatLabel={(v) => RATIO_LABEL[v]} + onChange={(value) => { + onLiveValue(channel, 'ratio', value); + commit({ ratio: value }); + }} + />
Ratio
`${v} dB`} - onChange={(value) => { - onLiveValue(channel, 'knee', value); - commit({ knee: value }); - }} -/> + className="compressor-panel__stepper compressor-panel__stepper--centered" + value={comp.knee} + values={KNEE_VALUES} + formatLabel={(v) => `${v} dB`} + onChange={(value) => { + onLiveValue(channel, 'knee', value); + commit({ knee: value }); + }} + />
Knee
diff --git a/src/features/dsp408/components/LimiterSection/LimiterSection.tsx b/src/features/dsp408/components/LimiterSection/LimiterSection.tsx index 2fb33f4..29df79a 100644 --- a/src/features/dsp408/components/LimiterSection/LimiterSection.tsx +++ b/src/features/dsp408/components/LimiterSection/LimiterSection.tsx @@ -206,10 +206,13 @@ function LimiterSection({ dsp, set_limiter }: { dsp: DSP; set_limiter: SetLimite
{outputs.map((output) => { const outputState = dsp.state.current_config.output_states[output]; - const idx = OUTPUT_METER_INDEX[output]; return ( - + ); })}
diff --git a/src/features/dsp408/components/Matrixsection/Matrixsection.css b/src/features/dsp408/components/Matrixsection/Matrixsection.css index 2fe0d56..ddd37e8 100644 --- a/src/features/dsp408/components/Matrixsection/Matrixsection.css +++ b/src/features/dsp408/components/Matrixsection/Matrixsection.css @@ -92,7 +92,7 @@ .matrix-panel__row .choice-stepper { align-self: center; - height: auto; + aspect-ratio: 3 / 1; flex: 0 0 auto; } @@ -109,4 +109,11 @@ .matrix-gain-stepper .choice-stepper__input { font-size: 2.8cqh; + text-align: center; } + +.matrix-gain-stepper .choice-stepper__arrows { + flex: 0 0 20%; + width: 20%; + padding-right: 0; +} \ No newline at end of file