diff --git a/9.1_FIX_TRACKUI.md b/9.1_FIX_TRACKUI.md new file mode 100644 index 0000000..e803ba3 --- /dev/null +++ b/9.1_FIX_TRACKUI.md @@ -0,0 +1,142 @@ +# Bug Fix Specification: Resolving Critical Row Desynchronization + +This document analyzes the root cause and provides a permanent structural solution to eliminate the vertical row desynchronization and internal horizontal scrolling artifacts occurring between the left Track Control Panel (TCP) and the right waveform lanes, based on the real-world visual analysis + +--- + +## 1. Visual Symptom Analysis + +The layout engine is suffering from two critical alignment failures indicated by the red arrows: + +```text +[ LEFT COLUMN - TCP PANEL ] [ RIGHT COLUMN - TIMELINE GRID ] +┌──────────────────────────────┐ ┌──────────────────────────────────────────────┐ +│ ... Track 05, 06 (Aligned) │ ══════════ │ Waveform 05, 06 (Aligned) │ +├──────────────────────────────┤ ├──────────────────────────────────────────────┤ +│ 07 Track 3 (Channel Header) │ [MISALIGNED]│ [EMPTY BLACK DEAD SPACE] (Lower red arrow) │ +│ [Junk horizontal scrollbar] │ ◄────────── │ ◄── Caused by Waveform 07 dropping height to 0│ +│ (Upper red arrow) │ ├──────────────────────────────────────────────┤ +├──────────────────────────────┤ │ Waveform 07 (Pushed down to Track 08's row) │ +│ 08 Track 3 │ ══════════ │ ... │ +└──────────────────────────────┘ └──────────────────────────────────────────────┘ + +``` + +### 1.1. Defect Index 1: Spurious Internal Horizontal Scrollbar (Upper Red Arrow) + +* **Symptom:** A small gray horizontal scrollbar emerges directly beneath Track 07 within the left TCP column. +* **Root Cause:** The container wrapper for the left TCP column enforces a rigid bounding layout (`fixed width` or missing an explicit `overflow-x: hidden` safety attribute). When inner structural components (such as long text labels, Mute/Solo clusters, or upload file actions) expand horizontally, the browser generates a local scrollbar. This automatically inflates the effective physical height of the left Track 07 by roughly $12\text{ px} \rightarrow 16\text{ px}$. + +### 1.2. Defect Index 2: Vertical Row Desynchronization & Dead Black Space (Lower Red Arrow) + +* **Symptom:** On the right column (Timeline), a massive horizontal empty black gap disrupts the grid layout where Waveform 07 ought to sit. Consequently, all matching waveforms for Track 07 and Track 08 are offset downward, falling entirely out of phase with their corresponding control headers on the left. +* **Root Cause:** The system evaluates the target height ($H$) of the left TCP container independently from the right Waveform Lane. When the left Track 07 column expands due to the rendering of the junk scrollbar, the right canvas lane does not dynamically adapt. This triggers a cumulative pixel error along the vertical axis ($Y$), producing progressive, severe desynchronization downstream (the lower the tracks sit, the worse the alignment drifts). + +--- + +## 2. Structural Correction Blueprint + +To prevent this layout defect from recurring—especially when porting the interface to desktop Python using PyQt/PySide—the system must completely decouple from independent height calculations and embrace a **Unified Row Layout** model. + +### 2.1. Standardized HTML / Tailwind CSS Architecture Blueprint + +Instead of splitting the page tree layout into two isolated columns (`Col1: [TCP1, TCP2, TCP3]` and `Col2: [Wave1, Wave2, Wave3]`), the application must encapsulate each matching TCP and Waveform pair within a shared, unified row wrapper: + +```html + +
+ + +
+ + +
+ +
+ + +
+ +
+ +
+ + +
+ +``` + +### Architectural Advantages: + +* Because both the control deck and the waveform graphic share an identical row container wrapper (`flex row` or `grid row`), any arbitrary height fluctuation on the TCP side (due to text zoom-in behaviors or overflow glitches) will instantly force the right waveform canvas view to mirror the $100\%$ row scale change. +* Only one master vertical scrollbar exists on the outer perimeter window to slide all rows simultaneously. + +--- + +## 3. Prevention Guidelines for Python Porting (PyQt6 / PySide6) + +If you attempt to design this DAW interface inside a containerized Python Docker application by leveraging two separate `QScrollArea` nodes for the TCP track column and the timeline canvas, you will inevitably trigger this row alignment defect due to timing delays or scroll tracking errors (`scrollEvent` mismatch). + +### 3.1. Secure Layout Architecture Using Python QWidget + +Implement a nested widget strategy to securely bind the horizontal axes together at all times: + +```python +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QScrollArea +from PyQt6.QtCore import Qt + +class ProDAWArrangeWindow(QWidget): + def __init__(self): + super().__init__() + self.main_layout = QVBoxLayout(self) + self.main_layout.setContentsMargins(0, 0, 0, 0) + self.main_layout.setSpacing(0) + + # 1. Instantiate a single, unified QScrollArea for the absolute Workspace + self.workspace_scroll = QScrollArea() + self.workspace_scroll.setWidgetResizable(True) + self.workspace_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn) + self.workspace_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + # 2. Outer container hosting the multi-channel rows + self.container_widget = QWidget() + self.container_layout = QVBoxLayout(self.container_widget) + self.container_layout.setContentsMargins(0, 0, 0, 0) + self.container_layout.setSpacing(0) + self.container_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + self.workspace_scroll.setWidget(self.container_widget) + self.main_layout.addWidget(self.workspace_scroll) + + def add_track(self, track_id: str): + """ + Appends a unified track row utilizing QHBoxLayout with a rigid physical height constraint. + """ + track_row = QWidget() + track_row.setFixedHeight(96) # Lock physical pixel height constraints for the row + + row_layout = QHBoxLayout(track_row) + row_layout.setContentsMargins(0, 0, 0, 0) + row_layout.setSpacing(0) + + # Left Panel: Track Control Panel (Enforces a strict rigid width constraint) + tcp_widget = QWidget() + tcp_widget.setFixedWidth(300) + # tcp_widget.setup_ui(...) + + # Right Panel: Waveform Canvas Viewport + waveform_widget = QWidget() + # waveform_widget.setup_canvas(...) + + # Combine both widgets into the layout block to guarantee row lock + row_layout.addWidget(tcp_widget) + row_layout.addWidget(waveform_widget) + + self.container_layout.addWidget(track_row) + +``` + +### 3.2. Concrete Advantages for Python Docker Environments + +* **Zero Alignment Variance:** Row alignment is entirely guaranteed at the OS-level layout engine, bypassing desynchronization issues caused by asynchronous rendering cycles or UI latency. +* **Streamlined UI Pipelines:** The environment tracking mechanisms hook into a single scrollbar, reducing memory usage and optimizing the drawing threads for the Docker X11 Server or WebRTC stream pipelines when projecting graphics down to the client. \ No newline at end of file diff --git a/9.2_UI_FIX.md b/9.2_UI_FIX.md new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/index.html b/app/templates/index.html index ed4686b..8e43598 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1671,32 +1671,32 @@ localDragStartTimeRef.current = time; }; - // Document-level mousemove/mouseup for local selection drag - useEffect(() => { - const handleMouseMove = (e) => { - if (!localDragInProgressRef.current) return; - const wrapper = timelineWrapperRef.current; - if (!wrapper) return; - const rect = wrapper.getBoundingClientRect(); - const scrollLeft = wrapper.scrollLeft; - const mouseX = e.clientX - rect.left + scrollLeft; - const time = Math.max(0, Math.min(maxDuration, mouseX / zoom)); - setLocalSelectionEnd(time); - }; - const handleMouseUp = () => { - if (localDragInProgressRef.current) { - localDragInProgressRef.current = false; - localDragTrackRef.current = null; - localDragStartTimeRef.current = 0; - } - }; - document.addEventListener('mousemove', handleMouseMove); - document.addEventListener('mouseup', handleMouseUp); - return () => { - document.removeEventListener('mousemove', handleMouseMove); - document.removeEventListener('mouseup', handleMouseUp); - }; - }, [zoom, maxDuration]); + // Document-level mousemove/mouseup for local selection drag + useEffect(() => { + const handleMouseMove = (e) => { + if (!localDragInProgressRef.current) return; + const wrapper = timelineWrapperRef.current; + if (!wrapper) return; + const rect = wrapper.getBoundingClientRect(); + const scrollLeft = wrapper.scrollLeft; + const mouseX = e.clientX - rect.left + scrollLeft; + const time = Math.max(0, Math.min(maxDuration, (mouseX - 300) / zoom)); + setLocalSelectionEnd(time); + }; + const handleMouseUp = () => { + if (localDragInProgressRef.current) { + localDragInProgressRef.current = false; + localDragTrackRef.current = null; + localDragStartTimeRef.current = 0; + } + }; + document.addEventListener('mousemove', handleMouseMove); + document.addEventListener('mouseup', handleMouseUp); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + document.removeEventListener('mouseup', handleMouseUp); + }; + }, [zoom, maxDuration]); const draggedClipRef = useRef(null); draggedClipRef.current = draggedClip; @@ -1742,7 +1742,7 @@ const rect = wrapper.getBoundingClientRect(); const scrollLeft = wrapper.scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft; - const time = mouseX / zoom; + const time = (mouseX - 300) / zoom; const rawStart = Math.max(0, time - drag.clickOffset); const newStart = snapTime(rawStart, snapValueRef.current, bpmRef.current); @@ -2766,79 +2766,118 @@ )} {/* ── Workspace ── */} -
- {/* TCP Left Column */} -
-
- Kênh - - {/* Timeline Toolbar */} -
- - - +
+
+ + {/* [ZONE A] Header Row */} +
+ {/* Left Side: Channels & Tools Header */} +
+ Kênh - {/* Separator */} + {/* Timeline Toolbar */} +
+ + + + + {/* Separator */} +
+ + {/* Quick Actions (Glue, Cut, Copy, Paste) */} + + + + +
+ + {/* Snap Section */}
- - {/* Quick Actions (Glue, Cut, Copy, Paste) */} - - - - +
+ Snap + +
+
+ {/* Right Side: Time Ruler */} +
+ {Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => { + const sec = i; + const x = sec * zoom; + return ( +
+ {formatTime(sec)} +
+ ); + })}
-
- {/* Tempo Track TCP - LOOP_EDITOR_2.md §6 */} -
+ {/* [ZONE A] Tempo Track Row */} +
+ {/* Left Side: Tempo TCP */} +
TM @@ -2858,124 +2897,93 @@
+ {/* Right Side: Tempo Lane */} +
+ +
+
+ {/* [ZONE B] Dynamic Track List Workspace */} +
{tracks.map((track, idx) => { const isSelected = selectedTrackId === track.id; return ( -
setSelectedTrackId(track.id)} - className={`h-[96px] p-2.5 flex flex-col justify-between transition-all relative cursor-pointer ${ - isSelected - ? 'bg-zinc-800/80 border-l-4 border-cyan-500 pl-1.5 ring-1 ring-cyan-500/30' - : 'hover:bg-zinc-800/30' - }`} - > -
-
- {(idx+1).toString().padStart(2, '0')} -
- - {track.name} - +
+ {/* Left Column: TCP */} +
setSelectedTrackId(track.id)} + className={`w-[300px] sticky left-0 z-10 p-2.5 flex flex-col justify-between bg-[#1e1e1e] border-r border-zinc-900 shrink-0 cursor-pointer border-l-4 overflow-hidden ${isSelected ? 'border-cyan-500 bg-[#252525]' : 'border-transparent hover:bg-zinc-800/20'}`} + > +
+
+ {(idx+1).toString().padStart(2, '0')} +
+ + {track.name} + +
+ +
+ + +
-
+
e.stopPropagation()}> + Mô phỏng: + onClick={() => generateSynthToTrack(track.id, 'kick')} + className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700" + >Kick Drum + onClick={() => generateSynthToTrack(track.id, 'synth')} + className="px-1 bg-zinc-800 hover:bg-zinc-700 rounded border border-zinc-700" + >Arpeggiator +
+ +
e.stopPropagation()}> +
+ updateTrackVolume(track.id, v)} /> + Gain: {Math.round(track.volume * 100)}% +
+ +
+ loadFileOnTrack(track.id, e.target.files[0])} + /> + +
-
e.stopPropagation()}> - Mô phỏng: - - -
- -
e.stopPropagation()}> -
- updateTrackVolume(track.id, v)} /> - Gain: {Math.round(track.volume * 100)}% -
- -
- loadFileOnTrack(track.id, e.target.files[0])} - /> - -
-
-
- ); - })} -
-
- - {/* Timeline Right Canvas */} -
-
- - {/* Ruler - LOOP_MAKER.md VÙNG A: Global Selection hitbox */} -
- {Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => { - const sec = i; - const x = sec * zoom; - return ( -
- {formatTime(sec)} -
- ); - })} -
- - {/* Stacked Lanes */} -
- {/* Tempo Track Lane - LOOP_EDITOR_2.md §6 */} -
- -
- - {tracks.map((track) => { - const isSelected = selectedTrackId === track.id; - return ( -
e.preventDefault()} onDrop={(e) => { e.preventDefault(); @@ -3003,19 +3011,22 @@ {track.buffer && (
)}
- ); - })} +
+ ); + })} - {/* Drop zone to create a new track during drag */} + {/* Bottom Drop Zone to create new track */} +
+
{ if (draggedClipRef.current) { const newId = addNewTrack(); @@ -3028,32 +3039,32 @@ Kéo clip xuống đây hoặc Click để tạo Track mới
+
- {/* Selection Overlay - only for Global mode (LOOP_EDITOR.md §1.2: local draws on canvas per-track) */} - {selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && ( -
-
handleHandleDragStart(e, 'left')} - title="Kéo giãn mốc bắt đầu"> -
-
-
handleHandleDragStart(e, 'right')} - title="Kéo giãn mốc kết thúc"> -
-
+ {/* Selection Overlay - only for Global mode (LOOP_EDITOR.md §1.2: local draws on canvas per-track) */} + {selectionMode !== 'local' && selLeft !== null && selRight !== null && selRight > selLeft && ( +
+
handleHandleDragStart(e, 'left')} + title="Kéo giãn mốc bắt đầu"> +
+
+
handleHandleDragStart(e, 'right')} + title="Kéo giãn mốc kết thúc"> +
- )} - - {/* Playhead */} -
-
+ )} + + {/* Playhead */} +
+