feat: prevent text selection on dsp sub pages
This commit is contained in:
@@ -15,6 +15,8 @@
|
|||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-size: 3cqh;
|
font-size: 3cqh;
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
text-align: left;
|
text-align: center;
|
||||||
|
|
||||||
color: var(--accent-brand);
|
color: var(--accent-brand);
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export interface LinearGraphProps {
|
|||||||
const defaultFormat = (v: number) => {
|
const defaultFormat = (v: number) => {
|
||||||
if (Math.abs(v) < 1e-9) return '0';
|
if (Math.abs(v) < 1e-9) return '0';
|
||||||
const abs = Math.abs(v);
|
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();
|
return parseFloat(v.toFixed(4)).toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,9 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: left;
|
align-items: left;
|
||||||
gap: 0.3cqh;
|
gap: 0.3cqh;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meter-bar__title {
|
.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 { InputChannel, OutputChannel } from '../../features/dsp408/types';
|
||||||
import { DSP408 } from '../../types';
|
import { DSP408 } from '../../types';
|
||||||
import {
|
import {
|
||||||
@@ -69,6 +69,14 @@ const MARGIN = 36;
|
|||||||
const LEFT_CHAIN_W = 560;
|
const LEFT_CHAIN_W = 560;
|
||||||
const RIGHT_CHAIN_W = 620;
|
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) {
|
function getRoutes(dsp: DSP408, output: OutputChannel) {
|
||||||
return dsp.state.current_config.output_states[output].matrix_routes;
|
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];
|
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({
|
function SignalFlowMatrix({
|
||||||
dsp,
|
dsp,
|
||||||
setMatrixRouting,
|
setMatrixRouting,
|
||||||
@@ -109,6 +129,20 @@ function SignalFlowMatrix({
|
|||||||
const inputNodeX = leftStart + LEFT_CHAIN_W;
|
const inputNodeX = leftStart + LEFT_CHAIN_W;
|
||||||
const outputNodeX = rightStart;
|
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 handleToggle = (output: OutputChannel, input: InputChannel) => {
|
||||||
const routes = getRoutes(dsp, output);
|
const routes = getRoutes(dsp, output);
|
||||||
const next = routes.connected.includes(input)
|
const next = routes.connected.includes(input)
|
||||||
@@ -124,6 +158,19 @@ function SignalFlowMatrix({
|
|||||||
const next =
|
const next =
|
||||||
Math.round(Math.min(MATRIX_GAIN_MAX, Math.max(MATRIX_GAIN_MIN, current + delta)) * 10) / 10;
|
Math.round(Math.min(MATRIX_GAIN_MAX, Math.max(MATRIX_GAIN_MIN, current + delta)) * 10) / 10;
|
||||||
void setMatrixGain(input, output, next);
|
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(() => {
|
const routePairs = useMemo(() => {
|
||||||
@@ -134,6 +181,18 @@ function SignalFlowMatrix({
|
|||||||
return pairs;
|
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 (
|
return (
|
||||||
<div className="signal-flow">
|
<div className="signal-flow">
|
||||||
<svg
|
<svg
|
||||||
@@ -141,7 +200,7 @@ function SignalFlowMatrix({
|
|||||||
preserveAspectRatio="xMidYMid meet"
|
preserveAspectRatio="xMidYMid meet"
|
||||||
className="signal-flow__svg"
|
className="signal-flow__svg"
|
||||||
>
|
>
|
||||||
{routePairs.map(({ input, output, i, j }) => {
|
{orderedRoutePairs.map(({ input, output, i, j }) => {
|
||||||
const connected = isRouted(dsp, output, input);
|
const connected = isRouted(dsp, output, input);
|
||||||
const y1 = inputCenterY(i);
|
const y1 = inputCenterY(i);
|
||||||
const y2 = outputCenterY(j);
|
const y2 = outputCenterY(j);
|
||||||
@@ -152,6 +211,20 @@ function SignalFlowMatrix({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<g key={`${input}-${output}`}>
|
<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
|
<path
|
||||||
d={path}
|
d={path}
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -159,9 +232,7 @@ function SignalFlowMatrix({
|
|||||||
strokeWidth={connected ? 2.5 : 1}
|
strokeWidth={connected ? 2.5 : 1}
|
||||||
strokeDasharray={connected ? undefined : '4 5'}
|
strokeDasharray={connected ? undefined : '4 5'}
|
||||||
opacity={connected ? 0.9 : 0.35}
|
opacity={connected ? 0.9 : 0.35}
|
||||||
style={{ cursor: 'pointer' }}
|
pointerEvents="none"
|
||||||
onClick={() => handleToggle(output, input)}
|
|
||||||
onWheel={connected ? (e) => handleWheelGain(e, output, input) : undefined}
|
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
@@ -252,8 +323,32 @@ function SignalFlowMatrix({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</svg>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SignalFlowMatrix;
|
export default SignalFlowMatrix;
|
||||||
@@ -9,6 +9,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gauge-track {
|
.gauge-track {
|
||||||
@@ -58,8 +61,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
touch-action: none;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.track {
|
.track {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compressor-panels {
|
.compressor-panels {
|
||||||
@@ -94,7 +97,7 @@
|
|||||||
|
|
||||||
.compressor-panel__cell--steppers {
|
.compressor-panel__cell--steppers {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: stretch;
|
||||||
gap: 1cqh;
|
gap: 1cqh;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +114,11 @@
|
|||||||
gap: 1cqh;
|
gap: 1cqh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compressor-panel__stepper-item .choice-stepper {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.compressor-panel__cell {
|
.compressor-panel__cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -145,6 +153,9 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.compressor-graph-panel {
|
.compressor-graph-panel {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-panels {
|
.delay-panels {
|
||||||
@@ -88,6 +91,9 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delay-graph-panel {
|
.delay-graph-panel {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gain-panels {
|
.gain-panels {
|
||||||
@@ -75,6 +77,9 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gain-graph-panel {
|
.gain-graph-panel {
|
||||||
@@ -168,6 +173,8 @@
|
|||||||
gap: 0.5cqh;
|
gap: 0.5cqh;
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.magnitude-selector__chip {
|
.magnitude-selector__chip {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gate-panels {
|
.gate-panels {
|
||||||
@@ -106,6 +109,9 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gate-graph-panel {
|
.gate-graph-panel {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.geq-panels {
|
.geq-panels {
|
||||||
@@ -114,6 +117,9 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.geq-graph-panel {
|
.geq-graph-panel {
|
||||||
|
|||||||
@@ -18,6 +18,9 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.limiter-panels {
|
.limiter-panels {
|
||||||
@@ -118,6 +121,9 @@
|
|||||||
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.limiter-graph-panel {
|
.limiter-graph-panel {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
|
||||||
border-top: var(--border-width) solid var(--border-hairline);
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.matrix-panels {
|
.matrix-panels {
|
||||||
@@ -87,10 +90,23 @@
|
|||||||
padding-inline: 3cqh;
|
padding-inline: 3cqh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.matrix-panel__row .choice-stepper {
|
||||||
|
align-self: center;
|
||||||
|
height: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.matrix-flow {
|
.matrix-flow {
|
||||||
flex: 0 0 calc(100% - var(--dsp-control-heigth));
|
flex: 0 0 calc(100% - var(--dsp-control-heigth));
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: var(--space-2);
|
padding: var(--space-2);
|
||||||
border-bottom: var(--border-width) solid var(--border-hairline);
|
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',
|
fontSize: '4cqh',
|
||||||
padding: 0,
|
padding: 0,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
marginTop: '2cqh',
|
marginTop: '3cqh',
|
||||||
marginBottom: '2cqh',
|
marginBottom: '3cqh',
|
||||||
}}
|
}}
|
||||||
onClick={() => handleToggle(output, input)}
|
onClick={() => handleToggle(output, input)}
|
||||||
>
|
>
|
||||||
@@ -110,6 +110,7 @@ function MatrixSection({
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ChoiceStepper
|
<ChoiceStepper
|
||||||
|
className="matrix-gain-stepper"
|
||||||
value={gain}
|
value={gain}
|
||||||
values={GAIN_VALUES}
|
values={GAIN_VALUES}
|
||||||
formatLabel={(v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
|
formatLabel={(v: number) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
|
||||||
|
|||||||
@@ -160,6 +160,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-dsp-form {
|
.add-dsp-form {
|
||||||
|
|||||||
Reference in New Issue
Block a user