feat: prevent text selection on dsp sub pages
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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,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,6 +323,30 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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`}
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
background: transparent;
|
||||
font-family: var(--font-mono);
|
||||
font-weight: var(--font-weight-normal);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.add-dsp-form {
|
||||
|
||||
Reference in New Issue
Block a user