@namespace DeepDrftPublic.Client.Controls
@using DeepDrftShared.Client.Common
@using DeepDrftPublic.Client.Services
@inject WaveformVisualizerControlState ControlState
@* The waveform visualizer control PANEL (Phase 12 §3d-revised → Phase 15 re-layout → Phase 15 polish).
The control deck for the lava-lamp visualizer: a deterministic THREE-ROW, sectioned layout that encodes
what the visualizer composes — a LAVA field and a WAVEFORM ribbon, each independently toggleable (§3):
Row 1 (MODE, always): lava lamp-toggle, waveform toggle (new waveform glyph), [collisions knob — only
when BOTH on], then the color knob pinned far-right (applies to the whole field, always visible).
Row 2 (LAVA, only when lava on): "LAVA:" label + gravity / heat / fluid-amount / fluid-viscosity.
Row 3 (WAVE, only when waveform on): "WAVE:" label + scroll RadialKnob + width RadialKnob (far-right).
The two toggles have STRONG ACTIVE-STATE styling: when ON the toggle chip has a green-accent background
(unmistakably active); when OFF it is muted/dim. The lava toggle keeps the lava-lamp glyph; the waveform
toggle uses a new distinct waveform-bars glyph (DDIcons.Waveform / WaveformFilled). Green = interactive
(§5 colour principle); light = non-interactive. All colours are token-sourced — no hardcoded hex.
This is the PANEL CONTENT hosted inside WaveformVisualizerControlPopover, now a screen-centered tinted
MudOverlay (Phase 15 §4). Because the overlay PORTALS its content out of this component's DOM subtree,
Blazor CSS isolation does not reach the rendered panel — so panel chrome AND the row/section layout live
in the GLOBAL deepdrft-styles.css (.waveform-visualizer-control-panel*), not the scoped .razor.css. The
scoped .razor.css carries only the legacy inline-bar fallback (Mix's old non-portaled mount), which may
now be dead post-Phase-12 but is left in place — flagged, not cut (out of Phase 15 scope).
It owns NO JS interop: it mutates the shared, session-scoped WaveformVisualizerControlState and raises
its Changed event. The visualizer bridge (WaveformVisualizer) subscribes and pushes the affected dial /
subsystem-enable to the WebGL module. RadialKnob has no icon slot (its Label renders as SVG text) and no
aria capture, so each control's Material icon rides beside its knob as a caption and the accessible name
rides on the wrapping group div (§7); the playful MudTooltip rides alongside for sighted hover. *@
@if (Visible)
{
@* ── Row 1 — MODE (always visible). ── *@
MODE:
@* Collisions are the interaction BETWEEN the two subsystems — meaningless with only one
present, so visible only when BOTH are on (§3 truth table). *@
@if (ControlState.LavaEnabled && ControlState.WaveformEnabled)
{
}
@if (ControlState.LavaEnabled && ControlState.WaveformEnabled)
{
}
@* ── Row 2 — WAVE section (only when waveform on). ── *@
@if (ControlState.WaveformEnabled)
{
WAVE:
}
@if (ControlState.LavaEnabled)
{
LAVA:
}
}
@code {
///
/// Whether the control deck is shown. The overlay host shows the panel whenever it is open, so the
/// default is true. Mix's legacy inline mount (if it survives) still feeds its lava-lamp toggle
/// into this — that mount always renders the component, and THIS component decides deck visibility
/// (Phase 10 §4): when false the rows are @if-gated out but the container holds its reserved height
/// (CSS min-height) so content below the inline bar never pops. Inside the overlay the host owns
/// open/closed, so the default keeps the panel populated.
///
[Parameter] public bool Visible { get; set; } = true;
///
/// When true, applies the waveform-visualizer-control-panel class to the root element,
/// enabling the global panel-chrome rules (NowPlayingCard chrome — square corners, lighter-navy
/// ground, thin light border — plus the row/section layout and pinned palette tokens). Set by
/// ; Mix's inline mount leaves this false so the
/// chrome never leaks onto the inline bar.
///
[Parameter] public bool PanelChrome { get; set; } = false;
private string _panelChromeClass => PanelChrome ? "waveform-visualizer-control-panel" : string.Empty;
// Each handler mutates its own dedicated property then raises Changed — the bridge re-reads and pushes
// the affected dial / subsystem-enable. All dial values are already normalized [0,1]; the bridge maps
// scroll speed to a visible time-span and routes the rest straight to the lava/colour dials. The two
// toggles flip a boolean (no value), driving the genuine per-subsystem draw-skip in the module (§6).
private void ToggleLava()
{
ControlState.LavaEnabled = !ControlState.LavaEnabled;
ControlState.NotifyChanged();
}
private void ToggleWaveform()
{
ControlState.WaveformEnabled = !ControlState.WaveformEnabled;
ControlState.NotifyChanged();
}
private void OnScrollSpeedChanged(double value)
{
ControlState.ScrollSpeed = value;
ControlState.NotifyChanged();
}
private void OnGradientRotationSpeedChanged(double value)
{
ControlState.GradientRotationSpeed = value;
ControlState.NotifyChanged();
}
private void OnLavaGravityChanged(double value)
{
ControlState.LavaGravity = value;
ControlState.NotifyChanged();
}
private void OnLavaHeatChanged(double value)
{
ControlState.LavaHeat = value;
ControlState.NotifyChanged();
}
private void OnFluidAmountChanged(double value)
{
ControlState.FluidAmount = value;
ControlState.NotifyChanged();
}
private void OnFluidViscosityChanged(double value)
{
ControlState.FluidViscosity = value;
ControlState.NotifyChanged();
}
private void OnCollisionStrengthChanged(double value)
{
ControlState.CollisionStrength = value;
ControlState.NotifyChanged();
}
private void OnWaveformWidthChanged(double value)
{
ControlState.WaveformWidth = value;
ControlState.NotifyChanged();
}
}