From 652c90979d45aca0579821c96ccadaa17a206552 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 15 Jun 2026 21:54:22 -0400 Subject: [PATCH] fix(visualizer): lift zoom slider out of fixed backdrop's stacking context so it receives pointer events again (P10 W1) --- .../Controls/MixWaveformVisualizer.razor | 37 +++++++++++-------- .../Controls/MixWaveformVisualizer.razor.cs | 1 + .../Controls/MixWaveformVisualizer.razor.css | 13 +++++-- .../Interop/visualizer/MixVisualizer.ts | 7 +++- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor index 191045e..15d4b35 100644 --- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor +++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor @@ -9,20 +9,25 @@
- - @* Viewing control only — never a seek surface. Hidden until a datum is present. *@ - @if (_hasDatum) - { -
- -
- }
+ +@* Viewing control only — never a seek surface. Hidden until a datum is present. + Deliberately a SIBLING of .mix-waveform-bg, not a child: the backdrop is position:fixed and so + forms its own stacking context, which would trap any descendant below the page's z-index:1 + foreground (.mix-detail-foreground) and let that foreground swallow the slider's pointer events. + As a top-level sibling with its own z-index, the slider stacks above the foreground and stays + draggable. *@ +@if (_hasDatum) +{ +
+ +
+} diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs index e7507fb..05cd367 100644 --- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs +++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.cs @@ -180,6 +180,7 @@ public partial class MixWaveformVisualizer : ComponentBase, IAsyncDisposable private async Task OnZoomFractionChanged(double fraction) { ZoomState.VisibleSeconds = MixZoomMapping.FractionToSeconds(fraction); + DebugLog($"zoom slider changed — raw fraction={fraction:F3} → visibleSeconds={ZoomState.VisibleSeconds:F3}s; pushing setZoom (handle={(_handle is null ? "null" : "ready")})."); await PushZoomAsync(); StateHasChanged(); } diff --git a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css index 4113ef7..97529cc 100644 --- a/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css +++ b/DeepDrftPublic.Client/Controls/MixWaveformVisualizer.razor.css @@ -21,13 +21,18 @@ } /* Zoom slider — a small viewing control pinned to the top-right, clear of the player bar at - the bottom and the nav bar at the top. Pointer events are re-enabled here only (the backdrop - stays inert), and it is never a seek surface. top: 5rem sits just below the fixed nav bar - (~4.5rem tall) so neither the expanded player bar nor the nav occludes it. */ + the bottom and the nav bar at the top. It is never a seek surface. top: 5rem sits just below the + fixed nav bar (~4.5rem tall) so neither the expanded player bar nor the nav occludes it. + + position: fixed (not absolute) because the slider is now a top-level sibling of the backdrop, not + a child of it — see the comment in the .razor. z-index: 10 lifts it above the page foreground + (.mix-detail-foreground, z-index: 1) so the foreground can't intercept its pointer events; that + occlusion was the resolution-slider regression. */ .mix-waveform-zoom { - position: absolute; + position: fixed; right: 1.5rem; top: 5rem; + z-index: 10; width: 180px; max-width: 40vw; pointer-events: auto; diff --git a/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts b/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts index ef66701..7e8fc73 100644 --- a/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts +++ b/DeepDrftPublic/Interop/visualizer/MixVisualizer.ts @@ -795,7 +795,12 @@ export function create(canvas: HTMLCanvasElement): MixVisualizerHandle { setZoom(seconds: number): void { // Clamp into the supported span so a stray value can't break the math. visibleSeconds = Math.min(MAX_VISIBLE_SECONDS, Math.max(MIN_VISIBLE_SECONDS, seconds)); - if (!playback.isPlaying) redrawOnce(); + // While playing, the running rAF loop uploads uVisibleSeconds next frame; while idle the + // loop is stopped (spec §E), so a zoom change must force one still frame here or the new + // span is uploaded only on the next unrelated redraw (theme/datum/resize) — i.e. never. + const idleRedraw = !playback.isPlaying; + debugLog(`setZoom — requested ${seconds.toFixed(3)}s, clamped ${visibleSeconds.toFixed(3)}s; idleRedraw=${idleRedraw} (isPlaying=${playback.isPlaying}).`); + if (idleRedraw) redrawOnce(); }, refreshTheme(): void {