feat: prevent text selection on dsp sub pages

This commit is contained in:
2026-09-12 15:19:57 +02:00
parent fd545fb2a8
commit 2717dfe99a
15 changed files with 174 additions and 13 deletions
+2
View File
@@ -15,6 +15,8 @@
font-weight: var(--font-weight-medium);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.btn:disabled {
@@ -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);
+1 -1
View File
@@ -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();
};
+3
View File
@@ -3,6 +3,9 @@
flex-direction: column;
align-items: left;
gap: 0.3cqh;
user-select: none;
-webkit-user-select: none;
}
.meter-bar__title {
@@ -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<GainTooltipState>({
visible: false,
x: 0,
y: 0,
label: '',
});
const hideTooltipTimer = useRef<ReturnType<typeof setTimeout> | 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 (
<div className="signal-flow">
<svg
@@ -141,7 +200,7 @@ function SignalFlowMatrix({
preserveAspectRatio="xMidYMid meet"
className="signal-flow__svg"
>
{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 (
<g key={`${input}-${output}`}>
{/* 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. */}
<path
d={path}
fill="none"
stroke="transparent"
strokeWidth={LINE_HITBOX_WIDTH}
style={{ cursor: 'pointer' }}
onClick={() => 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. */}
<path
d={path}
fill="none"
@@ -159,9 +232,7 @@ function SignalFlowMatrix({
strokeWidth={connected ? 2.5 : 1}
strokeDasharray={connected ? undefined : '4 5'}
opacity={connected ? 0.9 : 0.35}
style={{ cursor: 'pointer' }}
onClick={() => handleToggle(output, input)}
onWheel={connected ? (e) => handleWheelGain(e, output, input) : undefined}
pointerEvents="none"
/>
</g>
);
@@ -252,8 +323,32 @@ function SignalFlowMatrix({
);
})}
</svg>
{gainTooltip.visible && (
<div
className="signal-flow__gain-tooltip"
style={{
position: 'fixed',
left: gainTooltip.x + 14,
top: gainTooltip.y - 28,
pointerEvents: 'none',
zIndex: 50,
padding: '2px 8px',
borderRadius: 6,
fontSize: 12,
fontVariantNumeric: 'tabular-nums',
background: 'var(--surface-elevated, rgba(20, 20, 24, 0.92))',
color: 'var(--text-primary, #fff)',
border: '1px solid var(--border-hairline, #333)',
boxShadow: '0 2px 8px rgba(0,0,0,0.35)',
whiteSpace: 'nowrap',
}}
>
{gainTooltip.label}
</div>
)}
</div>
);
}
export default SignalFlowMatrix;
export default SignalFlowMatrix;
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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;
}
@@ -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({
</Button>
<ChoiceStepper
className="matrix-gain-stepper"
value={gain}
values={GAIN_VALUES}
formatLabel={(v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
+1
View File
@@ -160,6 +160,7 @@
background: transparent;
font-family: var(--font-mono);
font-weight: var(--font-weight-normal);
text-align: left;
}
.add-dsp-form {