From 501297d31e245ff9de3082c7fbe15a5e56397836 Mon Sep 17 00:00:00 2001 From: Boof2015 <75185879+Boof2015@users.noreply.github.com> Date: Sat, 28 Mar 2026 03:16:24 -0400 Subject: [PATCH] update the UI/UX for settings --- src/renderer/App.tsx | 29 +- src/renderer/components/BottomBar.tsx | 301 +++++++++-------- .../components/ScopeSettingsSection.tsx | 33 +- src/renderer/components/SettingsPanel.tsx | 44 ++- src/renderer/styles/globals.css | 310 +++++++++++------- 5 files changed, 452 insertions(+), 265 deletions(-) diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index a0e94e2..4c57c99 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -8,13 +8,14 @@ import { useSettingsStore } from './stores/settingsStore' import { useAudioStore } from './stores/audioStore' import { SCOPE_KINDS } from '../types/scope' -const SETTINGS_EXPAND_HEIGHT = 280 +const DEFAULT_SETTINGS_HEIGHT = 400 export default function App(): JSX.Element { const [toolbarVisible, setToolbarVisible] = useState(false) const [settingsOpen, setSettingsOpen] = useState(false) + const [settingsPanelHeight, setSettingsPanelHeight] = useState(0) + const [bottomBarHeight, setBottomBarHeight] = useState(0) const hideTimeoutRef = useRef | null>(null) - const prevSettingsOpenRef = useRef(false) const startupBoundsAppliedRef = useRef(false) const toggleScope = useSettingsStore((s) => s.toggleScope) @@ -40,15 +41,15 @@ export default function App(): JSX.Element { } }, []) - // Expand/collapse window by a fixed amount when settings toggle — no dynamic tracking - useEffect(() => { - if (settingsOpen && !prevSettingsOpenRef.current) { - window.electronAPI.expandSettings(SETTINGS_EXPAND_HEIGHT) - } else if (!settingsOpen && prevSettingsOpenRef.current) { - window.electronAPI.collapseSettings(SETTINGS_EXPAND_HEIGHT) - } - prevSettingsOpenRef.current = settingsOpen - }, [settingsOpen]) + const measuredSettingsHeight = settingsPanelHeight > 0 && bottomBarHeight > 0 + ? settingsPanelHeight + bottomBarHeight + : DEFAULT_SETTINGS_HEIGHT + + const settingsHeight = settingsOpen ? measuredSettingsHeight : 0 + + useLayoutEffect(() => { + window.electronAPI.setSettingsHeight(settingsHeight) + }, [settingsHeight]) const showToolbar = useCallback(() => { if (hideTimeoutRef.current) { @@ -137,9 +138,9 @@ export default function App(): JSX.Element { {settingsOpen && ( -
- - +
+ +
)}
diff --git a/src/renderer/components/BottomBar.tsx b/src/renderer/components/BottomBar.tsx index 3877397..a0fbee7 100644 --- a/src/renderer/components/BottomBar.tsx +++ b/src/renderer/components/BottomBar.tsx @@ -1,4 +1,4 @@ -import { useEffect, type CSSProperties, type JSX } from 'react' +import { useEffect, useLayoutEffect, useRef, type CSSProperties, type JSX } from 'react' import { useAudioStore } from '../stores/audioStore' import { useSettingsStore } from '../stores/settingsStore' import { useThemeStore, PRESETS, PRESET_IDS } from '../stores/themeStore' @@ -17,9 +17,11 @@ const SCOPE_LABELS: Record = { interface BottomBarProps { onClose: () => void + onHeightChange?: (height: number) => void } -export default function BottomBar({ onClose }: BottomBarProps): JSX.Element { +export default function BottomBar({ onClose, onHeightChange }: BottomBarProps): JSX.Element { + const rootRef = useRef(null) const hiddenScopes = useSettingsStore((s) => s.hiddenScopes) const toggleScope = useSettingsStore((s) => s.toggleScope) @@ -50,6 +52,33 @@ export default function BottomBar({ onClose }: BottomBarProps): JSX.Element { void refreshDevices() }, [refreshBackendSupport, refreshSystemSources, refreshDevices]) + useLayoutEffect(() => { + if (!onHeightChange || !rootRef.current) return + + const rootElement = rootRef.current + let frameId = 0 + + const reportHeight = (): void => { + cancelAnimationFrame(frameId) + frameId = requestAnimationFrame(() => { + onHeightChange(Math.ceil(rootElement.getBoundingClientRect().height)) + }) + } + + reportHeight() + + const resizeObserver = new ResizeObserver(() => { + reportHeight() + }) + + resizeObserver.observe(rootElement) + + return () => { + cancelAnimationFrame(frameId) + resizeObserver.disconnect() + } + }, [onHeightChange]) + const handleSourceChange = async (value: string): Promise => { if (value.startsWith('system:')) { const sourceId = value.slice('system:'.length) @@ -84,135 +113,149 @@ export default function BottomBar({ onClose }: BottomBarProps): JSX.Element { : 'Idle' return ( -
-
-
Modules
-
- {SCOPE_KINDS.map((kind) => { - const active = !hiddenScopes.has(kind) - return ( - - ) - })} +
+
+
+
+
Modules
+
+
+ {SCOPE_KINDS.map((kind) => { + const active = !hiddenScopes.has(kind) + return ( + + ) + })} +
+
+
+ +
+ +
+
Theme
+
+
+ {PRESET_IDS.map((id) => { + const preset = PRESETS[id] + const active = presetId === id && !customAccent + return ( + + )} +
+
+
+ +
+ +
+
Audio Source
+
+
+ + +
+ + {indicatorLabel} +
+
+ + {captureError ? ( +
{captureError}
+ ) : null} +
+
+ +
+ +
+
Trim
+
+
+ + {inputGainDb > 0 ? '+' : ''}{inputGainDb.toFixed(1)}dB + + setInputGain(Number(event.target.value))} + onDoubleClick={() => setInputGain(0)} + /> +
+
+
-
+
-
- -
-
Theme
-
- {PRESET_IDS.map((id) => { - const preset = PRESETS[id] - const active = presetId === id && !customAccent - return ( - - )} -
-
- -
- -
-
Audio Source
-
- - -
- - {indicatorLabel} -
-
- - {captureError ? ( -
{captureError}
- ) : null} -
- -
- -
-
Trim
-
- - {inputGainDb > 0 ? '+' : ''}{inputGainDb.toFixed(1)}dB - - setInputGain(Number(event.target.value))} - onDoubleClick={() => setInputGain(0)} - /> -
-
- - +
+ +
) } diff --git a/src/renderer/components/ScopeSettingsSection.tsx b/src/renderer/components/ScopeSettingsSection.tsx index f717c2b..9230801 100644 --- a/src/renderer/components/ScopeSettingsSection.tsx +++ b/src/renderer/components/ScopeSettingsSection.tsx @@ -74,6 +74,23 @@ function ToggleChip({ ) } +function ToggleGroup({ + label, + children, +}: { + label: string + children: ReactNode +}): JSX.Element { + return ( +
+ {label} +
+ {children} +
+
+ ) +} + function SelectControl({ label, value, @@ -175,7 +192,7 @@ export default function ScopeSettingsSection({ ))} -
+ onUpdate('spectrum', { showGrid: !current.showGrid })} /> -
+ -
+ onUpdate('oscilloscope', { showGrid: !current.showGrid })} /> -
+ Linear (Bi) -
+ onUpdate('vectorscope', { showGrid: !current.showGrid })} /> -
+ -
+ onUpdate('waveform', { multiband: !current.multiband })} /> -
+ void +} + +export default function SettingsPanel({ onHeightChange }: SettingsPanelProps): JSX.Element { const { scopeSettings, updateScopeSettings, hiddenScopes, scopeOrder, widthWeights, scopePopouts } = useSettingsStore() + const panelRef = useRef(null) + const scopeTrackRef = useRef(null) const dockedScopes = useMemo( () => scopeOrder.filter((kind) => !hiddenScopes.has(kind) && !scopePopouts[kind]?.poppedOut), @@ -16,9 +22,39 @@ export default function SettingsPanel(): JSX.Element { return { gridTemplateColumns } as CSSProperties }, [dockedScopes, widthWeights]) + useLayoutEffect(() => { + if (!onHeightChange || !panelRef.current) return + + const panelElement = panelRef.current + let frameId = 0 + + const reportHeight = (): void => { + cancelAnimationFrame(frameId) + frameId = requestAnimationFrame(() => { + onHeightChange(Math.ceil(panelElement.scrollHeight)) + }) + } + + reportHeight() + + const resizeObserver = new ResizeObserver(() => { + reportHeight() + }) + + resizeObserver.observe(panelElement) + if (scopeTrackRef.current) { + resizeObserver.observe(scopeTrackRef.current) + } + + return () => { + cancelAnimationFrame(frameId) + resizeObserver.disconnect() + } + }, [dockedScopes, onHeightChange, scopeTrackStyle]) + return ( -
-
+
+
{dockedScopes.map((kind) => (