From 2c35ab923d6aee4b861c63ee50ed2c9a16b4eb33 Mon Sep 17 00:00:00 2001 From: LucasDLTG Date: Thu, 23 Jul 2026 00:39:54 +0200 Subject: [PATCH] feat: fully responsive gain tab --- src/components/AutoScale/AutoScale.css | 22 +++ src/components/AutoScale/AutoScale.tsx | 78 ++++++++ src/components/Button/Button.tsx | 62 +++++- src/components/Sidebar/Sidebar.css | 2 +- src/components/Sidebar/Sidebar.tsx | 1 - src/components/Slider/VerticalSlider.css | 12 -- src/components/Slider/VerticalSlider.tsx | 178 ++++++++---------- .../VerticalStack/VerticalStack.css | 82 -------- .../VerticalStack/VerticalStack.tsx | 27 --- .../dsp408/GainSection/GainSection.css | 46 +++-- .../dsp408/GainSection/GainSection.tsx | 82 ++++---- src/pages/DspPage.tsx | 6 +- 12 files changed, 324 insertions(+), 274 deletions(-) create mode 100644 src/components/AutoScale/AutoScale.css create mode 100644 src/components/AutoScale/AutoScale.tsx delete mode 100644 src/components/VerticalStack/VerticalStack.css delete mode 100644 src/components/VerticalStack/VerticalStack.tsx diff --git a/src/components/AutoScale/AutoScale.css b/src/components/AutoScale/AutoScale.css new file mode 100644 index 0000000..a4c2d62 --- /dev/null +++ b/src/components/AutoScale/AutoScale.css @@ -0,0 +1,22 @@ +.autoscale-wrapper { + width: 100%; + height: 100%; + + min-width: 0; + min-height: 0; + + display: flex; + justify-content: center; + align-items: center; +} + +.autoscale-box { + position: relative; +} + +.autoscale-content { + position: absolute; + inset: 0; + + transform-origin: top left; +} diff --git a/src/components/AutoScale/AutoScale.tsx b/src/components/AutoScale/AutoScale.tsx new file mode 100644 index 0000000..ac2533c --- /dev/null +++ b/src/components/AutoScale/AutoScale.tsx @@ -0,0 +1,78 @@ +import { useEffect, useRef, useState } from 'react'; + +type AutoScaleProps = { + designWidth: number; + designHeight: number; + children: React.ReactNode; + className?: string; + style?: React.CSSProperties; +}; + +export default function AutoScale({ + designWidth, + designHeight, + children, + className, + style, +}: AutoScaleProps) { + const outerRef = useRef(null); + const [scale, setScale] = useState(1); + + useEffect(() => { + const el = outerRef.current; + if (!el) return; + + const ro = new ResizeObserver(([entry]) => { + const { width, height } = entry.contentRect; + + if (!width || !height) return; + + const nextScale = Math.min(width / designWidth, height / designHeight); + + setScale(nextScale > 0 ? nextScale : 1); + }); + + ro.observe(el); + + return () => ro.disconnect(); + }, [designWidth, designHeight]); + + return ( +
+
+
+ {children} +
+
+
+ ); +} diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 835f2db..f4a75f7 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,12 +1,42 @@ +// Button.tsx import './Button.css'; import type { ButtonHTMLAttributes, ReactNode } from 'react'; +import { useMemo } from 'react'; +import AutoScale from '../AutoScale/AutoScale'; type ButtonVariant = 'primary' | 'ghost' | 'toggle' | 'outline'; +// Design-space size per variant (same idea as DESIGN_WIDTH/HEIGHT in the slider) +const DEFAULT_DESIGN_SIZE: Record = { + primary: { width: 90, height: 36 }, + ghost: { width: 90, height: 36 }, + toggle: { width: 90, height: 40 }, + outline: { width: 90, height: 36 }, +}; + +const MIN_FONT_SIZE = 8; +const MAX_FONT_SIZE = 16; +const AVG_CHAR_WIDTH_RATIO = 0.6; +const HORIZONTAL_PADDING = 12; // reserved px inside the box, keeps text off the edges + +function getTextLength(children: ReactNode): number { + if (typeof children === 'string' || typeof children === 'number') { + return String(children).length; + } + if (Array.isArray(children)) { + return children.reduce((acc: number, c) => acc + getTextLength(c), 0); + } + return 6; // icons / non-text children: reasonable fallback estimate +} + interface ButtonProps extends ButtonHTMLAttributes { variant?: ButtonVariant; children: ReactNode; className?: string; + fontSize?: number; // explicit override, in design px — skips auto-calc + designWidth?: number; // override default design size if needed + designHeight?: number; + scale?: boolean; // wrap in AutoScale (default true) } export default function Button({ @@ -16,17 +46,47 @@ export default function Button({ disabled, onClick, type = 'button', + fontSize, + designWidth, + designHeight, + scale = true, + style, ...props }: ButtonProps) { - return ( + const { width: defaultW, height: defaultH } = DEFAULT_DESIGN_SIZE[variant]; + const W = designWidth ?? defaultW; + const H = designHeight ?? defaultH; + + const computedFontSize = useMemo(() => { + if (fontSize !== undefined) return fontSize; // user overload wins, no calc + + const textLength = getTextLength(children); + if (textLength === 0) return MAX_FONT_SIZE; + + const availableWidth = W - HORIZONTAL_PADDING; + const sizeByWidth = availableWidth / (textLength * AVG_CHAR_WIDTH_RATIO); + + return Math.min(MAX_FONT_SIZE, Math.max(MIN_FONT_SIZE, sizeByWidth)); + }, [fontSize, children, W]); + + const button = ( ); + + if (!scale) return button; + + return ( + + {button} + + ); } diff --git a/src/components/Sidebar/Sidebar.css b/src/components/Sidebar/Sidebar.css index 1768fdc..3ef1544 100644 --- a/src/components/Sidebar/Sidebar.css +++ b/src/components/Sidebar/Sidebar.css @@ -248,4 +248,4 @@ .app-root--sidebar-expanded .stage--with-sidebar { margin-left: var(--sidebar-width-expanded); -} \ No newline at end of file +} diff --git a/src/components/Sidebar/Sidebar.tsx b/src/components/Sidebar/Sidebar.tsx index 66d0e15..39e8050 100644 --- a/src/components/Sidebar/Sidebar.tsx +++ b/src/components/Sidebar/Sidebar.tsx @@ -1,4 +1,3 @@ -import { useState } from 'react'; import './Sidebar.css'; import { ChevronLeftIcon } from '../../assets/icons/ChevronLeftIcon'; diff --git a/src/components/Slider/VerticalSlider.css b/src/components/Slider/VerticalSlider.css index 350201f..9177cf0 100644 --- a/src/components/Slider/VerticalSlider.css +++ b/src/components/Slider/VerticalSlider.css @@ -1,15 +1,3 @@ -.gauge-wrapper { - position: relative; - width: 100%; - height: 100%; - min-width: 0; - min-height: 0; - - display: flex; - justify-content: center; - align-items: center; -} - .gauge-container { width: 70px; height: 260px; diff --git a/src/components/Slider/VerticalSlider.tsx b/src/components/Slider/VerticalSlider.tsx index b51cf97..5caf54e 100644 --- a/src/components/Slider/VerticalSlider.tsx +++ b/src/components/Slider/VerticalSlider.tsx @@ -2,6 +2,10 @@ import { useMemo, useRef, useState, useEffect } from 'react'; import './VerticalSlider.css'; +// Design size 260*70 + +const DESIGN_WIDTH = 70; + type VerticalSliderProps = { min: number; max: number; @@ -19,9 +23,6 @@ type VerticalSliderProps = { style?: React.CSSProperties; }; -const DESIGN_WIDTH = 70; -const DESIGN_HEIGHT = 260; - function getDecimals(n: number) { const s = n.toString(); const i = s.indexOf('.'); @@ -53,29 +54,11 @@ export default function VerticalSlider({ const rafRef = useRef(null); const dragging = useRef(false); const thumbRef = useRef(null); - - // --- NEW: auto-fit scale --- - const outerRef = useRef(null); - const [scale, setScale] = useState(1); + const keyboardEditing = useRef(false); useEffect(() => { - const el = outerRef.current; - if (!el) return; + if (keyboardEditing.current) return; - const ro = new ResizeObserver((entries) => { - const { width, height } = entries[0].contentRect; - if (width === 0 || height === 0) return; - - const s = Math.min(width / DESIGN_WIDTH, height / DESIGN_HEIGHT); - setScale(s > 0 ? s : 1); - }); - - ro.observe(el); - return () => ro.disconnect(); - }, []); - // --------------------------- - - useEffect(() => { setDisplayValue(value); dragValue.current = value; }, [value]); @@ -129,21 +112,40 @@ export default function VerticalSlider({ function handleKeyDown(e: React.KeyboardEvent) { if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return; + + keyboardEditing.current = true; + e.preventDefault(); - let next = value; + const base = keyboardValue.current ?? value; + + let next = base; + if (e.key === 'ArrowUp' || e.key === 'ArrowRight') next += currentStep; + if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') next -= currentStep; - updateValue(next); - keyboardValue.current = normalizeValue(next); + next = normalizeValue(next); + + keyboardValue.current = next; + + updateValue(next); // updates displayValue only } function handleKeyUp() { - if (keyboardValue.current !== null) { - onChange?.(keyboardValue.current); - keyboardValue.current = null; + if (keyboardValue.current == null) return; + + keyboardEditing.current = false; + + const next = keyboardValue.current; + + if (controlledValue === undefined) { + setInternalValue(next); } + + onChange?.(next); + + keyboardValue.current = null; } function valueFromPointer(clientY: number) { @@ -190,78 +192,56 @@ export default function VerticalSlider({ const percent = (value - min) / (max - min); return ( -
-
-
-
-
- {Array.from({ length: 15 }).map((_, i) => )} -
- -
-
-
-
- -
- {Array.from({ length: 15 }).map((_, i) => )} -
+
+
+
+ {Array.from({ length: 15 }).map((_, i) => ( + + ))}
-
- setEditingText(value.toFixed(decimals))} - onChange={(e) => setEditingText(e.target.value)} - onBlur={commitEditing} - onKeyDown={(e) => { - if (e.key === 'Enter') inputRef.current?.blur(); - if (e.key === 'Escape') { - setEditingText(null); - inputRef.current?.blur(); - } - }} - /> - {unit} +
+
+
+
+ +
+ {Array.from({ length: 15 }).map((_, i) => ( + + ))}
+ +
+ setEditingText(value.toFixed(decimals))} + onChange={(e) => setEditingText(e.target.value)} + onBlur={commitEditing} + onKeyDown={(e) => { + if (e.key === 'Enter') inputRef.current?.blur(); + if (e.key === 'Escape') { + setEditingText(null); + inputRef.current?.blur(); + } + }} + /> + {unit} +
-
-); -} \ No newline at end of file + ); +} diff --git a/src/components/VerticalStack/VerticalStack.css b/src/components/VerticalStack/VerticalStack.css deleted file mode 100644 index 5231564..0000000 --- a/src/components/VerticalStack/VerticalStack.css +++ /dev/null @@ -1,82 +0,0 @@ -.vertical-stack { - height: 100%; - box-sizing: border-box; - display: flex; - flex-direction: column; - min-width: 0; - min-height: 0; - - display: flex; - flex-direction: column; - - border: 1px solid var(--border-hairline); - - border-radius: var(--radius-md); -} - -/* header */ - -.vertical-stack__title { - flex: 0 0 auto; - - padding: var(--space-1); - - text-align: center; - - font-family: var(--font-ui); - - font-weight: 600; - - color: var(--text-primary); - - border-bottom: 1px solid var(--border-hairline); - - line-height: 1.2; -} - -/* area containing components */ - -.vertical-stack__content { - flex: 1; - - min-height: 0; - - display: flex; - - flex-direction: column; -} - -/* each component slot */ - -.vertical-stack__item { - flex: 1; - - min-height: 0; - - display: flex; - - justify-content: center; - - align-items: center; - - overflow: hidden; - - width: 100%; -} - -/* - The child keeps its natural dimensions. - Only horizontal scaling happens. -*/ - -.vertical-stack__scale { - width: 100%; - - display: flex; - - justify-content: center; - - align-items: center; - - transform-origin: center; -} diff --git a/src/components/VerticalStack/VerticalStack.tsx b/src/components/VerticalStack/VerticalStack.tsx deleted file mode 100644 index daff02a..0000000 --- a/src/components/VerticalStack/VerticalStack.tsx +++ /dev/null @@ -1,27 +0,0 @@ -// VerticalStack.tsx -import React from 'react'; -import './VerticalStack.css'; - -type VerticalStackProps = { - title: string; - titleFontSize?: number; - children: React.ReactNode; -}; - -export default function VerticalStack({ title, children }: VerticalStackProps) { - return ( -
-
- {title} -
- -
- {React.Children.map(children, (child, index) => ( -
- {child} -
- ))} -
-
- ); -} \ No newline at end of file diff --git a/src/components/dsp408/GainSection/GainSection.css b/src/components/dsp408/GainSection/GainSection.css index 6fddb71..7370cd4 100644 --- a/src/components/dsp408/GainSection/GainSection.css +++ b/src/components/dsp408/GainSection/GainSection.css @@ -1,5 +1,5 @@ :root { - --gain-section-height: 40vh; /* your fixed % of screen height */ + --gain-section-height: 50vh; /* your fixed % of screen height */ } .gain-section { @@ -33,26 +33,22 @@ display: flex; flex-direction: row; align-items: stretch; - justify-content: safe center; + justify-content: safe center; /* centers when it fits, falls back to start-aligned scroll when it overflows */ gap: var(--space-4); - - scrollbar-width: thin; - scrollbar-color: var(--border-hairline) transparent; - width: 100%; height: 100%; min-width: 0; min-height: 0; - overflow-x: auto; overflow-y: hidden; } -.gain-panels > * { - height: 100%; - aspect-ratio: 90 / 340; /* your panel's design ratio (title+slider+buttons) — tune */ + +.gain-panel-frame { flex: 0 0 auto; - min-width: 0; - min-height: 0; + height: calc(100% - 8px); + aspect-ratio: var(--panel-w) / var(--panel-h); /* now driven by JS, not duplicated */ + margin: 4px 0; + box-sizing: border-box; } .gain-panels::-webkit-scrollbar { @@ -65,3 +61,29 @@ .gain-panels::-webkit-scrollbar-track { background: transparent; } + +.gain-panel { + /* width/height now set inline from DESIGN_WIDTH/HEIGHT, drop the fixed px here */ + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + border: 1px solid var(--border-hairline); + border-radius: var(--radius-md); + box-sizing: border-box; + padding: 8px; +} + +.gain-panel__title { + width: 100%; + + text-align: center; + + font-family: var(--font-ui); + font-size: 12px; + font-weight: 600; + + padding-bottom: 6px; + + border-bottom: 1px solid var(--border-hairline); +} diff --git a/src/components/dsp408/GainSection/GainSection.tsx b/src/components/dsp408/GainSection/GainSection.tsx index c966de0..ddd3df5 100644 --- a/src/components/dsp408/GainSection/GainSection.tsx +++ b/src/components/dsp408/GainSection/GainSection.tsx @@ -1,4 +1,3 @@ -import VerticalStack from '../../VerticalStack/VerticalStack'; import VerticalSlider from '../../Slider/VerticalSlider'; import { invoke } from '@tauri-apps/api/core'; import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State'; @@ -6,8 +5,12 @@ import { DSP } from '../../../types/types'; import { NotificationType } from '../../../types/types'; import { useState } from 'react'; import Button from '../../Button/Button'; +import AutoScale from '../../AutoScale/AutoScale'; import './GainSection.css'; +const DESIGN_WIDTH = 90; +const DESIGN_HEIGHT = 380; + function GainPanel({ dsp, notify, @@ -89,32 +92,43 @@ function GainPanel({ }; return ( - - - {/* +
+ +
+
{title}
- */} - + + + + + +
+
+
); } @@ -131,14 +145,14 @@ function GainSection({ { channel: { Input: InputChannel.InC }, title: 'In C' }, { channel: { Input: InputChannel.InD }, title: 'In D' }, - { channel: { Output: OutputChannel.Out1 }, title: "Out 1" }, - { channel: { Output: OutputChannel.Out2 }, title: "Out 2" }, - { channel: { Output: OutputChannel.Out3 }, title: "Out 3" }, - { channel: { Output: OutputChannel.Out4 }, title: "Out 4" }, - { channel: { Output: OutputChannel.Out5 }, title: "Out 5" }, - { channel: { Output: OutputChannel.Out6 }, title: "Out 6" }, - { channel: { Output: OutputChannel.Out7 }, title: "Out 7" }, - { channel: { Output: OutputChannel.Out8 }, title: "Out 8" }, + { channel: { Output: OutputChannel.Out1 }, title: 'Out 1' }, + { channel: { Output: OutputChannel.Out2 }, title: 'Out 2' }, + { channel: { Output: OutputChannel.Out3 }, title: 'Out 3' }, + { channel: { Output: OutputChannel.Out4 }, title: 'Out 4' }, + { channel: { Output: OutputChannel.Out5 }, title: 'Out 5' }, + { channel: { Output: OutputChannel.Out6 }, title: 'Out 6' }, + { channel: { Output: OutputChannel.Out7 }, title: 'Out 7' }, + { channel: { Output: OutputChannel.Out8 }, title: 'Out 8' }, ]; return ( diff --git a/src/pages/DspPage.tsx b/src/pages/DspPage.tsx index eb71aac..71a3f7c 100644 --- a/src/pages/DspPage.tsx +++ b/src/pages/DspPage.tsx @@ -47,11 +47,7 @@ function DSPPage({ }; return ( -
+