From 2717dfe99a2fd32064018575c90189d4652af8c3 Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Sat, 12 Sep 2026 15:19:57 +0200 Subject: [PATCH] feat: prevent text selection on dsp sub pages --- src/components/Button/Button.css | 2 + .../ChoiceStepper/ChoiceStepper.css | 2 +- src/components/LinearGraph/LinearGraph.tsx | 2 +- src/components/MeterBar/MeterBar.css | 3 + .../SignalFlowMatrix/SignalFlowMatrix.tsx | 107 +++++++++++++++++- .../VerticalSlider/VerticalSlider.css | 5 +- .../CompressorSection/CompressorSection.css | 13 ++- .../components/DelaySection/DelaySection.css | 6 + .../components/GainSection/GainSection.css | 7 ++ .../components/GateSection/GateSection.css | 6 + .../components/GeqSection/GeqSection.css | 6 + .../LimiterSection/LimiterSection.css | 6 + .../Matrixsection/Matrixsection.css | 16 +++ .../Matrixsection/Matrixsection.tsx | 5 +- src/pages/AddDspPage/AddDspPage.css | 1 + 15 files changed, 174 insertions(+), 13 deletions(-) diff --git a/src/components/Button/Button.css b/src/components/Button/Button.css index bd18a9d..e78b16f 100644 --- a/src/components/Button/Button.css +++ b/src/components/Button/Button.css @@ -15,6 +15,8 @@ font-weight: var(--font-weight-medium); cursor: pointer; + user-select: none; + -webkit-user-select: none; } .btn:disabled { diff --git a/src/components/ChoiceStepper/ChoiceStepper.css b/src/components/ChoiceStepper/ChoiceStepper.css index 677228a..cdbd41b 100644 --- a/src/components/ChoiceStepper/ChoiceStepper.css +++ b/src/components/ChoiceStepper/ChoiceStepper.css @@ -20,7 +20,7 @@ font-family: var(--font-mono); font-size: 3cqh; font-weight: var(--font-weight-normal); - text-align: left; + text-align: center; color: var(--accent-brand); diff --git a/src/components/LinearGraph/LinearGraph.tsx b/src/components/LinearGraph/LinearGraph.tsx index 509c94b..defd054 100644 --- a/src/components/LinearGraph/LinearGraph.tsx +++ b/src/components/LinearGraph/LinearGraph.tsx @@ -55,7 +55,7 @@ export interface LinearGraphProps { const defaultFormat = (v: number) => { if (Math.abs(v) < 1e-9) return '0'; const abs = Math.abs(v); - if (abs >= 1000 || abs < 0.01) return v.toExponential(1); + if (abs >= 100000 || abs < 0.01) return v.toExponential(1); return parseFloat(v.toFixed(4)).toString(); }; diff --git a/src/components/MeterBar/MeterBar.css b/src/components/MeterBar/MeterBar.css index 7fa55d2..7830c5b 100644 --- a/src/components/MeterBar/MeterBar.css +++ b/src/components/MeterBar/MeterBar.css @@ -3,6 +3,9 @@ flex-direction: column; align-items: left; gap: 0.3cqh; + + user-select: none; + -webkit-user-select: none; } .meter-bar__title { diff --git a/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx b/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx index faf2439..29f918a 100644 --- a/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx +++ b/src/components/SignalFlowMatrix/SignalFlowMatrix.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { InputChannel, OutputChannel } from '../../features/dsp408/types'; import { DSP408 } from '../../types'; import { @@ -69,6 +69,14 @@ const MARGIN = 36; const LEFT_CHAIN_W = 560; const RIGHT_CHAIN_W = 620; +// Width of the invisible "hitbox" stroke drawn under each visible line, in SVG +// user units. Much wider than the visible 1–2.5px line so it's easy to click +// or catch with a scroll wheel without needing pixel-perfect aim. +const LINE_HITBOX_WIDTH = 14; + +// How long the gain tooltip stays visible after the last wheel tick, in ms. +const GAIN_TOOLTIP_HOLD_MS = 700; + function getRoutes(dsp: DSP408, output: OutputChannel) { return dsp.state.current_config.output_states[output].matrix_routes; } @@ -79,6 +87,18 @@ function getGain(dsp: DSP408, output: OutputChannel, input: InputChannel) { return getRoutes(dsp, output).gains[input]; } +function formatGainDb(value: number) { + const sign = value > 0 ? '+' : ''; + return `${sign}${value.toFixed(1)} dB`; +} + +type GainTooltipState = { + visible: boolean; + x: number; + y: number; + label: string; +}; + function SignalFlowMatrix({ dsp, setMatrixRouting, @@ -109,6 +129,20 @@ function SignalFlowMatrix({ const inputNodeX = leftStart + LEFT_CHAIN_W; const outputNodeX = rightStart; + const [gainTooltip, setGainTooltip] = useState({ + visible: false, + x: 0, + y: 0, + label: '', + }); + const hideTooltipTimer = useRef | null>(null); + + useEffect(() => { + return () => { + if (hideTooltipTimer.current) clearTimeout(hideTooltipTimer.current); + }; + }, []); + const handleToggle = (output: OutputChannel, input: InputChannel) => { const routes = getRoutes(dsp, output); const next = routes.connected.includes(input) @@ -124,6 +158,19 @@ function SignalFlowMatrix({ const next = Math.round(Math.min(MATRIX_GAIN_MAX, Math.max(MATRIX_GAIN_MIN, current + delta)) * 10) / 10; void setMatrixGain(input, output, next); + + // Show a small tooltip with the new gain value right next to the cursor, + // then let it fade away shortly after wheeling stops. + if (hideTooltipTimer.current) clearTimeout(hideTooltipTimer.current); + setGainTooltip({ + visible: true, + x: e.clientX, + y: e.clientY, + label: formatGainDb(next), + }); + hideTooltipTimer.current = setTimeout(() => { + setGainTooltip((t) => ({ ...t, visible: false })); + }, GAIN_TOOLTIP_HOLD_MS); }; const routePairs = useMemo(() => { @@ -134,6 +181,18 @@ function SignalFlowMatrix({ return pairs; }, []); + // Draw dashed (unconnected) links first so connected (solid) links always + // paint on top of them, regardless of input/output index order. This has + // to be recomputed whenever routing changes, since isRouted reads live + // dsp state — unlike routePairs above, which is just static index pairs. + const orderedRoutePairs = useMemo(() => { + return [...routePairs].sort((a, b) => { + const aConnected = isRouted(dsp, a.output, a.input) ? 1 : 0; + const bConnected = isRouted(dsp, b.output, b.input) ? 1 : 0; + return aConnected - bConnected; + }); + }, [routePairs, dsp]); + return (
- {routePairs.map(({ input, output, i, j }) => { + {orderedRoutePairs.map(({ input, output, i, j }) => { const connected = isRouted(dsp, output, input); const y1 = inputCenterY(i); const y2 = outputCenterY(j); @@ -152,6 +211,20 @@ function SignalFlowMatrix({ return ( + {/* Invisible wide-stroke hitbox: carries the click/wheel handlers so + the line is easy to grab without needing to hit the thin visible + stroke exactly. Sits directly under the visible path. */} + handleToggle(output, input)} + onWheel={connected ? (e) => handleWheelGain(e, output, input) : undefined} + /> + {/* Visible line, purely decorative — no pointer events, so it never + competes with the hitbox above for hover/click/wheel. */} handleToggle(output, input)} - onWheel={connected ? (e) => handleWheelGain(e, output, input) : undefined} + pointerEvents="none" /> ); @@ -252,8 +323,32 @@ function SignalFlowMatrix({ ); })} + + {gainTooltip.visible && ( +
+ {gainTooltip.label} +
+ )}
); } -export default SignalFlowMatrix; +export default SignalFlowMatrix; \ No newline at end of file diff --git a/src/components/VerticalSlider/VerticalSlider.css b/src/components/VerticalSlider/VerticalSlider.css index a8839e3..9fcb04b 100644 --- a/src/components/VerticalSlider/VerticalSlider.css +++ b/src/components/VerticalSlider/VerticalSlider.css @@ -9,6 +9,9 @@ align-items: center; box-sizing: border-box; + + user-select: none; + -webkit-user-select: none; } .gauge-track { @@ -58,8 +61,6 @@ height: 100%; cursor: pointer; - touch-action: none; - user-select: none; } .track { diff --git a/src/features/dsp408/components/CompressorSection/CompressorSection.css b/src/features/dsp408/components/CompressorSection/CompressorSection.css index 7219e58..ba287c0 100644 --- a/src/features/dsp408/components/CompressorSection/CompressorSection.css +++ b/src/features/dsp408/components/CompressorSection/CompressorSection.css @@ -18,6 +18,9 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } .compressor-panels { @@ -94,7 +97,7 @@ .compressor-panel__cell--steppers { flex-direction: column; - justify-content: center; + justify-content: stretch; gap: 1cqh; } @@ -111,6 +114,11 @@ gap: 1cqh; } +.compressor-panel__stepper-item .choice-stepper { + flex: 0 0 auto; + height: auto; +} + .compressor-panel__cell { display: flex; flex-direction: column; @@ -145,6 +153,9 @@ box-sizing: border-box; background: var(--bg-panel); + + user-select: none; + -webkit-user-select: none; } .compressor-graph-panel { diff --git a/src/features/dsp408/components/DelaySection/DelaySection.css b/src/features/dsp408/components/DelaySection/DelaySection.css index 16f334c..fc89cab 100644 --- a/src/features/dsp408/components/DelaySection/DelaySection.css +++ b/src/features/dsp408/components/DelaySection/DelaySection.css @@ -19,6 +19,9 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } .delay-panels { @@ -88,6 +91,9 @@ box-sizing: border-box; background: var(--bg-panel); + + user-select: none; + -webkit-user-select: none; } .delay-graph-panel { diff --git a/src/features/dsp408/components/GainSection/GainSection.css b/src/features/dsp408/components/GainSection/GainSection.css index bee7773..c83072b 100644 --- a/src/features/dsp408/components/GainSection/GainSection.css +++ b/src/features/dsp408/components/GainSection/GainSection.css @@ -19,6 +19,8 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + user-select: none; + -webkit-user-select: none; } .gain-panels { @@ -75,6 +77,9 @@ box-sizing: border-box; background: var(--bg-panel); + + user-select: none; + -webkit-user-select: none; } .gain-graph-panel { @@ -168,6 +173,8 @@ gap: 0.5cqh; overflow: hidden; + user-select: none; + -webkit-user-select: none; } .magnitude-selector__chip { diff --git a/src/features/dsp408/components/GateSection/GateSection.css b/src/features/dsp408/components/GateSection/GateSection.css index 0f52ef0..ed7eac8 100644 --- a/src/features/dsp408/components/GateSection/GateSection.css +++ b/src/features/dsp408/components/GateSection/GateSection.css @@ -20,6 +20,9 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } .gate-panels { @@ -106,6 +109,9 @@ box-sizing: border-box; background: var(--bg-panel); + + user-select: none; + -webkit-user-select: none; } .gate-graph-panel { diff --git a/src/features/dsp408/components/GeqSection/GeqSection.css b/src/features/dsp408/components/GeqSection/GeqSection.css index 6baf86f..02374ac 100644 --- a/src/features/dsp408/components/GeqSection/GeqSection.css +++ b/src/features/dsp408/components/GeqSection/GeqSection.css @@ -19,6 +19,9 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } .geq-panels { @@ -114,6 +117,9 @@ box-sizing: border-box; background: var(--bg-panel); + + user-select: none; + -webkit-user-select: none; } .geq-graph-panel { diff --git a/src/features/dsp408/components/LimiterSection/LimiterSection.css b/src/features/dsp408/components/LimiterSection/LimiterSection.css index ca290ac..49b00f6 100644 --- a/src/features/dsp408/components/LimiterSection/LimiterSection.css +++ b/src/features/dsp408/components/LimiterSection/LimiterSection.css @@ -18,6 +18,9 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } .limiter-panels { @@ -118,6 +121,9 @@ box-sizing: border-box; background: var(--bg-panel); + + user-select: none; + -webkit-user-select: none; } .limiter-graph-panel { diff --git a/src/features/dsp408/components/Matrixsection/Matrixsection.css b/src/features/dsp408/components/Matrixsection/Matrixsection.css index c917ec4..785f21e 100644 --- a/src/features/dsp408/components/Matrixsection/Matrixsection.css +++ b/src/features/dsp408/components/Matrixsection/Matrixsection.css @@ -19,6 +19,9 @@ z-index: 20; border-top: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } .matrix-panels { @@ -87,10 +90,23 @@ padding-inline: 3cqh; } +.matrix-panel__row .choice-stepper { + align-self: center; + height: auto; + flex: 0 0 auto; +} + .matrix-flow { flex: 0 0 calc(100% - var(--dsp-control-heigth)); min-height: 0; box-sizing: border-box; padding: var(--space-2); border-bottom: var(--border-width) solid var(--border-hairline); + + user-select: none; + -webkit-user-select: none; } + +.matrix-gain-stepper .choice-stepper__input { + font-size: 2.8cqh; +} \ No newline at end of file diff --git a/src/features/dsp408/components/Matrixsection/Matrixsection.tsx b/src/features/dsp408/components/Matrixsection/Matrixsection.tsx index a550996..59bf129 100644 --- a/src/features/dsp408/components/Matrixsection/Matrixsection.tsx +++ b/src/features/dsp408/components/Matrixsection/Matrixsection.tsx @@ -101,8 +101,8 @@ function MatrixSection({ fontSize: '4cqh', padding: 0, minHeight: 0, - marginTop: '2cqh', - marginBottom: '2cqh', + marginTop: '3cqh', + marginBottom: '3cqh', }} onClick={() => handleToggle(output, input)} > @@ -110,6 +110,7 @@ function MatrixSection({ `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`} diff --git a/src/pages/AddDspPage/AddDspPage.css b/src/pages/AddDspPage/AddDspPage.css index 321ba06..f7a93c6 100644 --- a/src/pages/AddDspPage/AddDspPage.css +++ b/src/pages/AddDspPage/AddDspPage.css @@ -160,6 +160,7 @@ background: transparent; font-family: var(--font-mono); font-weight: var(--font-weight-normal); + text-align: left; } .add-dsp-form {