improve UX

This commit is contained in:
Boof2015
2026-05-28 02:19:29 -04:00
parent c9d741c979
commit 281f84ec46
13 changed files with 91 additions and 30 deletions
+1
View File
@@ -16,6 +16,7 @@ class LUFSMeterEngine : public ScopeEngine
public:
const char* scopeId() const override { return "lufsmeter"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 360, 340, 240, 220 }; }
void setSampleRate(double sampleRate) override
{
+1
View File
@@ -11,6 +11,7 @@ class OscilloscopeEngine : public ScopeEngine
public:
const char* scopeId() const override { return "oscilloscope"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 760, 300, 320, 160 }; }
void setSampleRate(double sampleRate) override
{
+20 -3
View File
@@ -84,7 +84,8 @@ namespace
.withInitialisationData("prismScope", juce::String(scopeId))
.withEventListener("prismConfig", [&editor](juce::var v) { editor.onPrismConfig(std::move(v)); })
.withEventListener("prismReady", [&editor](juce::var) { editor.onPrismReady(); })
.withEventListener("prismSpectrogramConfig", [&editor](juce::var v) { editor.onScopeNativeConfig(std::move(v)); });
.withEventListener("prismSpectrogramConfig", [&editor](juce::var v) { editor.onScopeNativeConfig(std::move(v)); })
.withEventListener("prismSettingsPanel", [&editor](juce::var v) { editor.onSettingsPanel(std::move(v)); });
#if ! PRISM_USE_DEV_SERVER
options = options.withResourceProvider([](const auto& url) { return provideResource(url); });
#endif
@@ -109,9 +110,11 @@ PrismSpectrumEditor::PrismSpectrumEditor(PrismSpectrumProcessor& p)
webView.goToURL(juce::WebBrowserComponent::getResourceProviderRoot());
#endif
// Per-scope sizing: open at the scope's preferred default, with a per-scope min.
const auto pref = engine->preferredSize();
setResizable(true, true);
setResizeLimits(360, 200, 4096, 4096);
setSize(900, 480);
setResizeLimits(pref.minWidth, pref.minHeight, 4096, 4096);
setSize(pref.defaultWidth, pref.defaultHeight);
// Drive frames at the display's refresh rate (adapts to 60/120/144 Hz).
vblank = juce::VBlankAttachment(this, [this] { renderFrame(); });
@@ -122,6 +125,20 @@ void PrismSpectrumEditor::resized()
webView.setBounds(getLocalBounds());
}
void PrismSpectrumEditor::onSettingsPanel(juce::var payload)
{
const int height = juce::jmax(0, (int) payload.getProperty("height", 0));
const int delta = height - settingsPanelHeight;
if (delta == 0)
return;
settingsPanelHeight = height;
// Grow/shrink the window by exactly the panel height so the scope area is
// unchanged. Deliberately NOT touching resize limits — doing so makes JUCE's
// constrainer snap the window and double the applied size.
setSize(getWidth(), getHeight() + delta);
}
void PrismSpectrumEditor::onPrismConfig(juce::var payload)
{
const auto settings = payload.getProperty("settings", juce::var());
+8
View File
@@ -30,6 +30,11 @@ public:
// Scope-specific native config (e.g. the spectrogram's canvas-derived rowCount).
void onScopeNativeConfig(juce::var payload);
// The UI's settings panel opened/closed along the bottom. Grow/shrink the editor
// height by exactly the panel height so the scope area is unchanged (the window
// accommodates the panel, like the app). 0 = closed.
void onSettingsPanel(juce::var payload);
// Push the processor's saved settings to the UI (used on ready + on host
// state restore, to cover either ordering).
void pushRestoreSettings();
@@ -47,6 +52,9 @@ private:
std::vector<float> drainLeft, drainRight;
double lastSampleRate = 0.0;
// Height (px) the editor is currently grown by for the open settings panel.
int settingsPanelHeight = 0;
// One-time attempt to lift WKWebView's private 60fps cap (macOS).
bool frameRateUncapped = false;
int uncapAttempts = 0;
+2
View File
@@ -62,6 +62,8 @@ private:
juce::CriticalSection settingsLock;
juce::String settingsJson;
std::atomic<int> editorWidth { 0 };
std::atomic<int> editorHeight { 0 };
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PrismSpectrumProcessor)
};
+10
View File
@@ -13,6 +13,16 @@ class ScopeEngine
public:
virtual ~ScopeEngine() = default;
/** Editor window sizing (logical px). Each scope picks a shape that fits it. */
struct PreferredSize
{
int defaultWidth = 720;
int defaultHeight = 320;
int minWidth = 320;
int minHeight = 160;
};
virtual PreferredSize preferredSize() const { return {}; }
/** Stable id sent to the webview (via initialisation data) to pick the UI scope. */
virtual const char* scopeId() const = 0;
+1
View File
@@ -22,6 +22,7 @@ class SpectrogramEngine : public ScopeEngine
public:
const char* scopeId() const override { return "spectrogram"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 700, 340, 360, 200 }; }
void setSampleRate(double sampleRate) override
{
+1
View File
@@ -9,6 +9,7 @@ class SpectrumEngine : public ScopeEngine
public:
const char* scopeId() const override { return "spectrum"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 820, 320, 360, 180 }; }
void setSampleRate(double sampleRate) override
{
+1
View File
@@ -15,6 +15,7 @@ class VUMeterEngine : public ScopeEngine
public:
const char* scopeId() const override { return "vumeter"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 480, 300, 280, 180 }; }
void setSampleRate(double sampleRate) override
{
+1
View File
@@ -17,6 +17,7 @@ class VectorscopeEngine : public ScopeEngine
public:
const char* scopeId() const override { return "vectorscope"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 440, 440, 240, 200 }; }
void setSampleRate(double sampleRate) override
{
+1
View File
@@ -22,6 +22,7 @@ public:
const char* scopeId() const override { return "waveform"; }
juce::Identifier frameEventId() const override { return frameId; }
PreferredSize preferredSize() const override { return { 720, 260, 360, 140 }; }
void setSampleRate(double sr) override
{
+27 -14
View File
@@ -1,10 +1,16 @@
import { useState, type JSX, type ReactNode } from 'react'
import { useEffect, useState, type JSX, type ReactNode } from 'react'
import type { ScopeKind } from '../types/scope'
import type { ScopeSettings } from '../types/settings'
import type { PrismResolvedTheme } from '../types/theme'
import ScopeSettingsSection from '../renderer/components/ScopeSettingsSection'
import GearIcon from './GearIcon'
import { useScopeHostSync } from './useScopeHostSync'
import { emitToHost } from './juceBridge'
// Height (CSS px) of the bottom settings panel. The C++ editor grows its window by
// exactly this when settings open (and shrinks back on close) so the scope area is
// unchanged — like the desktop app. Must match `.spectrum-app__panel` in styles.css.
const PANEL_HEIGHT = 280
interface ScopeAppProps<K extends ScopeKind> {
kind: K
@@ -13,29 +19,36 @@ interface ScopeAppProps<K extends ScopeKind> {
}
/**
* Generic plugin shell for any scope: hosts the canvas + a gear-toggled settings
* drawer (the reused ScopeSettingsSection), wired to host sync via useScopeHostSync.
* Generic plugin shell for any scope: the scope fills the viewport, and the gear
* toggles a settings panel that opens along the bottom (like the desktop app). The
* window grows by the panel height to accommodate it, so the scope area never resizes.
*/
export default function ScopeApp<K extends ScopeKind>({ kind, renderScope }: ScopeAppProps<K>): JSX.Element {
const { settings, resolvedTheme, handleUpdate } = useScopeHostSync(kind)
const [settingsOpen, setSettingsOpen] = useState(false)
useEffect(() => {
emitToHost('prismSettingsPanel', { height: settingsOpen ? PANEL_HEIGHT : 0 })
}, [settingsOpen])
return (
<div className="spectrum-app">
{renderScope(settings, resolvedTheme)}
<div className="spectrum-app__viewport">
{renderScope(settings, resolvedTheme)}
<button
type="button"
className={`spectrum-app__gear ${settingsOpen ? 'is-active' : ''}`.trim()}
onClick={() => setSettingsOpen((open) => !open)}
aria-label="Settings"
title="Settings"
>
<GearIcon />
</button>
<button
type="button"
className={`spectrum-app__gear ${settingsOpen ? 'is-active' : ''}`.trim()}
onClick={() => setSettingsOpen((open) => !open)}
aria-label="Settings"
title="Settings"
>
<GearIcon />
</button>
</div>
{settingsOpen && (
<div className="spectrum-app__settings">
<div className="spectrum-app__panel">
<ScopeSettingsSection
kind={kind}
settings={settings}
+17 -13
View File
@@ -22,11 +22,19 @@ body {
}
.spectrum-app {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
/* The scope fills everything above the (optional) bottom settings panel. */
.spectrum-app__viewport {
position: relative;
flex: 1 1 auto;
min-height: 0;
}
.spectrum-scope {
position: absolute;
inset: 0;
@@ -76,21 +84,17 @@ body {
border-color: var(--control-border-active, rgba(56, 189, 248, 0.4));
}
.spectrum-app__settings {
position: absolute;
top: 44px;
right: 8px;
z-index: 5;
width: 320px;
max-width: calc(100% - 16px);
max-height: calc(100% - 56px);
/* Settings panel along the bottom (like the app). The editor window grows by exactly
this height when it opens, so the scope viewport above keeps its size. The fixed
basis MUST match PANEL_HEIGHT in ScopeApp.tsx (the grow delta), or the scope would
shift. Content taller than this scrolls. */
.spectrum-app__panel {
flex: 0 0 280px;
width: 100%;
overflow-y: auto;
padding: 12px;
background: var(--settings-bg-top, rgba(8, 10, 14, 0.96));
border: 1px solid var(--panel-outline, rgba(255, 255, 255, 0.12));
border-radius: 10px;
box-shadow: var(--panel-shadow, 0 14px 34px rgba(0, 0, 0, 0.4));
backdrop-filter: blur(12px);
border-top: 1px solid var(--panel-outline, rgba(255, 255, 255, 0.12));
}
.spectrum-scope__peak {