feat: dont moove slider cursor if tcp command fails

This commit is contained in:
2026-07-28 13:53:57 +02:00
parent 54fda168cc
commit eab62e151e
6 changed files with 376 additions and 353 deletions
@@ -1,8 +1,6 @@
import VerticalSlider from '../Slider/VerticalSlider';
import { invoke } from '@tauri-apps/api/core';
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
import { DSP } from '../../../types/types';
import { NotificationType } from '../../../types/types';
import Button from '../../Button/Button';
import ChannelGroup from '../ChannelGroup/ChannelGroup';
import { LinearGraph } from '../LinearGraph/LinearGraph';
@@ -12,109 +10,62 @@ import './GainSection.css';
const PANEL_DESIGN_WIDTH = 90;
const PANEL_DESIGN_HEIGHT = 400;
function ChannelGroupScaled({ title, children }: { title: string; children: React.ReactNode }) {
const wrapperRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
// function ChannelGroupScaled({ title, children }: { title: string; children: React.ReactNode }) {
// const wrapperRef = useRef<HTMLDivElement>(null);
// const [scale, setScale] = useState(1);
useEffect(() => {
const el = wrapperRef.current;
if (!el) return;
const observer = new ResizeObserver((entries) => {
const { height } = entries[0].contentRect;
if (height === 0) return;
setScale(height / PANEL_DESIGN_HEIGHT); // height is the only free axis here
});
observer.observe(el);
return () => observer.disconnect();
}, []);
// useEffect(() => {
// const el = wrapperRef.current;
// if (!el) return;
// const observer = new ResizeObserver((entries) => {
// const { height } = entries[0].contentRect;
// if (height === 0) return;
// setScale(height / PANEL_DESIGN_HEIGHT); // height is the only free axis here
// });
// observer.observe(el);
// return () => observer.disconnect();
// }, []);
return (
<div ref={wrapperRef} style={{ height: '100%', '--panel-scale': scale } as React.CSSProperties}>
<ChannelGroup title={title}>{children}</ChannelGroup>
</div>
);
}
// return (
// <div ref={wrapperRef} style={{ height: '100%', '--panel-scale': scale } as React.CSSProperties}>
// <ChannelGroup title={title}>{children}</ChannelGroup>
// </div>
// );
// }
function GainPanel({
dsp,
notify,
channel,
title,
setGain,
setMute,
setInverse,
}: {
dsp: DSP;
notify: (message: string, type?: NotificationType, dspName?: string) => void;
channel: Channel;
title: string;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) {
const setGain = async (value: number) => {
try {
const success = await invoke<boolean>('set_channel_gain', {
id: dsp.id,
channel,
db: value,
});
const channelState =
'Input' in channel
? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output];
if (success) {
notify(`Input A gain set to ${value} dB`, 'success', dsp.name);
} else {
notify('Failed to set input gain', 'error', dsp.name);
}
} catch (err) {
notify(`Gain update failed: ${String(err)}`, 'error', dsp.name);
}
};
const muted = channelState?.mute ?? false;
const inverted = channelState?.phase_inverted ?? false;
const gain = channelState?.gain ?? 0;
const setMute = async (muted: boolean) => {
try {
const success = await invoke<boolean>('set_mute', {
id: dsp.id,
channel,
muted,
});
if (success) {
notify(`Input A ${muted ? 'muted' : 'unmuted'}`, 'success', dsp.name);
} else {
notify('Failed to change mute state', 'error', dsp.name);
}
} catch (err) {
notify(`Mute update failed: ${String(err)}`, 'error', dsp.name);
}
};
const setInverse = async (inverted: boolean) => {
try {
const success = await invoke<boolean>('set_channel_inverse_gain', {
id: dsp.id,
channel,
inverted,
});
if (success) {
notify(`Input A phase ${inverted ? 'inverted' : 'normal'}`, 'success', dsp.name);
} else {
notify('Failed to change phase', 'error', dsp.name);
}
} catch (err) {
notify(`Phase update failed: ${String(err)}`, 'error', dsp.name);
}
};
const [muted, setMuted] = useState(false);
const [inverted, setInverted] = useState(false);
const viewportRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
const toggleMute = async () => {
const next = !muted;
setMuted(next);
await setMute(next);
await setMute(channel, !muted);
};
const toggleInverse = async () => {
const next = !inverted;
setInverted(next);
await setInverse(next);
await setInverse(channel, !inverted);
};
useEffect(() => {
@@ -148,7 +99,8 @@ function GainPanel({
switchStepValue={-10}
switchStep={0.1}
unit=" dB"
onChange={setGain}
value={gain}
onChange={(value) => setGain(channel, value)}
style={{ width: 70, height: 260, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink
/>
@@ -169,10 +121,14 @@ function GainPanel({
function GainSection({
dsp,
notify,
setGain,
setMute,
setInverse,
}: {
dsp: DSP;
notify: (message: string, type?: NotificationType, dspName?: string) => void;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) {
const inputs: { channel: Channel; title: string }[] = [
{ channel: { Input: InputChannel.InA }, title: 'In A' },
@@ -223,13 +179,29 @@ function GainSection({
<div className="gain-panels">
<ChannelGroup title="Input">
{inputs.map(({ channel, title }) => (
<GainPanel key={title} dsp={dsp} notify={notify} channel={channel} title={title} />
<GainPanel
key={title}
dsp={dsp}
channel={channel}
title={title}
setGain={setGain}
setMute={setMute}
setInverse={setInverse}
/>
))}
</ChannelGroup>
<ChannelGroup title="Output">
{outputs.map(({ channel, title }) => (
<GainPanel key={title} dsp={dsp} notify={notify} channel={channel} title={title} />
<GainPanel
key={title}
dsp={dsp}
channel={channel}
title={title}
setGain={setGain}
setMute={setMute}
setInverse={setInverse}
/>
))}
</ChannelGroup>
</div>
@@ -1,35 +1,35 @@
import VerticalSlider from '../Slider/VerticalSlider';
import { invoke } from '@tauri-apps/api/core';
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
import { DSP } from '../../../types/types';
import { NotificationType } from '../../../types/types';
// import VerticalSlider from '../Slider/VerticalSlider';
// import { invoke } from '@tauri-apps/api/core';
// import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
// import { DSP } from '../../../types/types';
// import { NotificationType } from '../../../types/types';
import ChannelGroup from '../ChannelGroup/ChannelGroup';
import './GateSection.css';
// import ChannelGroup from '../ChannelGroup/ChannelGroup';
// import './GateSection.css';
function GateSection({
dsp,
notify,
}: {
dsp: DSP;
notify: (message: string, type?: NotificationType, dspName?: string) => void;
}) {
console.log(dsp, notify);
return (
<div className="rerere">
<VerticalSlider
min={-60}
max={12}
step={0.5}
switchStepValue={-10}
switchStep={0.1}
unit=" dB"
onChange={(_f: number) => {
return;
}}
/>
</div>
);
}
// function GateSection({
// dsp,
// notify,
// }: {
// dsp: DSP;
// notify: (message: string, type?: NotificationType, dspName?: string) => void;
// }) {
// console.log(dsp, notify);
// return (
// <div className="rerere">
// <VerticalSlider
// min={-60}
// max={12}
// step={0.5}
// switchStepValue={-10}
// switchStep={0.1}
// unit=" dB"
// onChange={(_f: number) => {
// return;
// }}
// />
// </div>
// );
// }
export default GateSection;
// export default GateSection;
+68 -49
View File
@@ -13,8 +13,8 @@ type VerticalSliderProps = {
switchStepValue?: number;
switchStep?: number;
value?: number;
onChange?: (value: number) => void;
value: number;
onChange?: (value: number) => Promise<boolean>;
unit?: string;
@@ -34,23 +34,18 @@ export default function VerticalSlider({
step,
switchStepValue,
switchStep,
value: controlledValue,
value,
onChange,
unit = '',
className,
style,
}: VerticalSliderProps) {
const [internalValue, setInternalValue] = useState(min);
const value = controlledValue ?? internalValue;
const [editingText, setEditingText] = useState<string | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const sliderRef = useRef<HTMLDivElement>(null);
const lastValue = useRef(value);
const keyboardValue = useRef<number | null>(null);
const [displayValue, setDisplayValue] = useState(value);
const dragValue = useRef(value);
const rafRef = useRef<number | null>(null);
const dragging = useRef(false);
const thumbRef = useRef<HTMLDivElement>(null);
const keyboardEditing = useRef(false);
@@ -74,16 +69,20 @@ export default function VerticalSlider({
}, []);
useEffect(() => {
if (dragging.current) return;
if (keyboardEditing.current) return;
setDisplayValue(value);
dragValue.current = value;
}, [value]);
const currentStep =
switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue
? switchStep
: step;
function getStep(value: number) {
if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) {
return switchStep;
}
return step;
}
const decimals = useMemo(
() => Math.max(getDecimals(step), switchStep !== undefined ? getDecimals(switchStep) : 0),
@@ -92,30 +91,25 @@ export default function VerticalSlider({
function normalizeValue(input: number) {
const clamped = Math.min(max, Math.max(min, input));
const snapped = Math.round(clamped / currentStep) * currentStep;
return Number(snapped.toFixed(6));
const currentStep = getStep(clamped);
const snapped = min + Math.round((clamped - min) / currentStep) * currentStep;
return Number(Math.min(max, Math.max(min, snapped)).toFixed(6));
}
function updateValue(v: number) {
const next = normalizeValue(v);
function updateValue(input: number) {
const next = normalizeValue(input);
dragValue.current = next;
lastValue.current = next;
if (thumbRef.current) {
const percent = (next - min) / (max - min);
thumbRef.current.style.bottom = `${percent * 100}%`;
}
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(() => {
setDisplayValue(dragValue.current);
rafRef.current = null;
});
}
setDisplayValue(next);
}
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return;
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
return;
}
keyboardEditing.current = true;
@@ -123,33 +117,39 @@ export default function VerticalSlider({
const base = keyboardValue.current ?? value;
const currentStep = getStep(base);
let next = base;
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') next += currentStep;
if (e.key === 'ArrowUp' || e.key === 'ArrowRight') {
next += currentStep;
}
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') next -= currentStep;
if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') {
next -= currentStep;
}
next = normalizeValue(next);
keyboardValue.current = next;
updateValue(next); // updates displayValue only
updateValue(next);
}
function handleKeyUp() {
async function handleKeyUp() {
if (keyboardValue.current == null) return;
keyboardEditing.current = false;
const next = keyboardValue.current;
if (controlledValue === undefined) {
setInternalValue(next);
}
onChange?.(next);
const success = await onChange?.(next);
keyboardEditing.current = false;
keyboardValue.current = null;
if (success === false) {
setDisplayValue(value);
dragValue.current = value;
}
}
function valueFromPointer(clientY: number) {
@@ -172,28 +172,47 @@ export default function VerticalSlider({
valueFromPointer(e.clientY);
}
function endDrag(e: React.PointerEvent<HTMLDivElement>) {
dragging.current = false;
async function endDrag(e: React.PointerEvent<HTMLDivElement>) {
if (!dragging.current) return;
const finalValue = dragValue.current;
if (controlledValue === undefined) setInternalValue(finalValue);
setDisplayValue(finalValue);
onChange?.(finalValue);
const success = await onChange?.(finalValue);
dragging.current = false;
if (success === false) {
setDisplayValue(value);
dragValue.current = value;
}
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
e.currentTarget.releasePointerCapture(e.pointerId);
}
}
function commitEditing() {
async function commitEditing() {
if (editingText !== null && editingText !== '' && editingText !== '-') {
const parsed = Number(editingText);
if (!Number.isNaN(parsed)) updateValue(parsed);
if (!Number.isNaN(parsed)) {
const next = normalizeValue(parsed);
setDisplayValue(next);
const success = await onChange?.(next);
if (success === false) {
setDisplayValue(value);
dragValue.current = value;
}
}
}
setEditingText(null);
}
const percent = (value - min) / (max - min);
const percent = (displayValue - min) / (max - min);
return (
<div