diff --git a/src/components/MainContent.css b/src/components/MainContent.css index 31bc118..2357b90 100644 --- a/src/components/MainContent.css +++ b/src/components/MainContent.css @@ -10,7 +10,7 @@ .main-content-wrapper { display: flex; flex-direction: column; - min-width: calc(200px + var(--max-number-of-bars) * var(--track-grid-bar-width)); /* info width + grid width */ + min-width: calc(var(--track-info-panel-width) + var(--max-number-of-bars) * var(--track-grid-bar-width)); /* info width + grid width */ min-height: fit-content; position: relative; } @@ -19,7 +19,7 @@ position: fixed; top: 50px; left: 0; - width: 200px; + width: var(--track-info-panel-width); height: 20px; background-color: #2d2d2d; border-bottom: 1px solid #3a3a3a; @@ -42,7 +42,7 @@ border-bottom: 1px solid #3a3a3a; background-color: #2d2d2d; z-index: 20; - margin-left: 200px; /* Offset for info-container */ + margin-left: var(--track-info-panel-width); /* Offset for info-container */ width: calc(var(--max-number-of-bars) * var(--track-grid-bar-width)); /* Exact width for 32 bars */ cursor: pointer; /* Show pointer cursor on hover to indicate interactivity */ } @@ -80,7 +80,7 @@ .info-container { position: sticky; left: 0; - width: 200px; + width: var(--track-info-panel-width); z-index: 1002; background-color: #2d2d2d; align-self: flex-start; diff --git a/src/components/MainContent.tsx b/src/components/MainContent.tsx index 0d9e6eb..28eefc5 100644 --- a/src/components/MainContent.tsx +++ b/src/components/MainContent.tsx @@ -50,6 +50,8 @@ const MainContent: React.FC = ({ addAudioTrack, projectName, savedProjectName, + requestPianoRollScroll, + mainContentScrollRequest, } = useProjectStore(); // State to store regions @@ -136,8 +138,10 @@ const MainContent: React.FC = ({ ) || 40; const playheadPixel = barPosition * barWidth; - // Center the playhead in the visible grid area (excluding the 200px sticky info panel) - const infoWidth = 200; + // Center the playhead in the visible grid area (excluding the sticky info panel) + const infoWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--track-info-panel-width') + ) || 200; const targetScrollLeft = playheadPixel - (container.clientWidth - infoWidth) / 2; const clampedScrollLeft = Math.max( 0, @@ -148,6 +152,36 @@ const MainContent: React.FC = ({ container.scrollLeft = clampedScrollLeft; }, [playheadPosition, isPlaying, autoScrollEnabled, timeSignature]); + // Handle scroll requests from piano roll header clicks + useEffect(() => { + if (mainContentScrollRequest === null) return; + + const container = mainContentRef.current; + if (!container) return; + + const beatsPerBar = timeSignature.numerator; + const barPosition = mainContentScrollRequest / beatsPerBar; + const barWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--track-grid-bar-width') + ) || 40; + const playheadPixel = barPosition * barWidth; + + // Center the playhead in the visible grid area (excluding the sticky info panel) + const infoWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--track-info-panel-width') + ) || 200; + const targetScrollLeft = playheadPixel - (container.clientWidth - infoWidth) / 2; + const clampedScrollLeft = Math.max( + 0, + Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth) + ); + + container.scrollLeft = clampedScrollLeft; + + // Clear the request after handling + useProjectStore.setState({ mainContentScrollRequest: null }); + }, [mainContentScrollRequest, timeSignature]); + // Effect to verify track updates useEffect(() => { // Check for pending updates @@ -774,6 +808,7 @@ const MainContent: React.FC = ({ const clickPosition = calculatePlayheadFromMouse(e.clientX); if (clickPosition !== null) { setPlayheadPosition(clickPosition); + requestPianoRollScroll(clickPosition); if (DEBUG_MODE.MAIN_CONTENT) { console.log(`Single click on bar numbers - Set playhead to: ${clickPosition}`); @@ -799,7 +834,7 @@ const MainContent: React.FC = ({ document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; - }, [calculateBarIndexFromMouse, calculatePlayheadFromMouse, setPlayheadPosition]); + }, [calculateBarIndexFromMouse, calculatePlayheadFromMouse, setPlayheadPosition, requestPianoRollScroll]); const { showInstrumentSelection, isLooping, loopingRange } = useProjectStore(); diff --git a/src/components/piano-roll/PianoGridHeader.tsx b/src/components/piano-roll/PianoGridHeader.tsx index ddd2827..441b8df 100644 --- a/src/components/piano-roll/PianoGridHeader.tsx +++ b/src/components/piano-roll/PianoGridHeader.tsx @@ -14,7 +14,7 @@ const PianoGridHeader: React.FC = ({ timeSignature = { numerator: 4, denominator: 4 } // Default to 4/4 if not provided }) => { // Get store access for playhead position updates - const { setPlayheadPosition } = useProjectStore(); + const { setPlayheadPosition, requestMainContentScroll } = useProjectStore(); // Refs for drag functionality const isDraggingRef = useRef(false); @@ -122,8 +122,9 @@ const PianoGridHeader: React.FC = ({ console.log(`Current bar: ${currentBarNumber} (beat ${currentPlayheadPosition})`); console.log(`Destination bar: ${destinationBarNumber} (beat ${newPosition})`); } - + setPlayheadPosition(newPosition); + requestMainContentScroll(newPosition); } }; diff --git a/src/components/piano-roll/PianoRoll.tsx b/src/components/piano-roll/PianoRoll.tsx index 4306aaa..1f53d01 100644 --- a/src/components/piano-roll/PianoRoll.tsx +++ b/src/components/piano-roll/PianoRoll.tsx @@ -41,7 +41,7 @@ const PianoRoll: React.FC = ({ }) => { const isSpectrogram = mode === 'spectrogram'; const isHybrid = mode === 'hybrid'; - const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm } = useProjectStore(); + const { maxBars, tracks, updateTrack, timeSignature, showChatBox, showInstrumentSelection, keySignature, selectedMode, setSelectedMode, playheadPosition, isPlaying, autoScrollEnabled, bpm, pianoRollScrollRequest } = useProjectStore(); // Tool state for piano roll const [activeTool, setActiveTool] = useState<'pointer' | 'pencil'>('pointer'); @@ -689,6 +689,34 @@ const PianoRoll: React.FC = ({ container.scrollLeft = clampedScrollLeft; }, [playheadPosition, isPlaying, autoScrollEnabled]); + // Handle scroll requests from main content bar numbers clicks + useEffect(() => { + if (pianoRollScrollRequest === null) return; + + const container = pianoRollContentRef.current; + if (!container) return; + + const beatWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--region-grid-beat-width') + ) || 40; + const playheadPixel = pianoRollScrollRequest * beatWidth; + + // Center the playhead in the visible grid area (excluding the 60px sticky piano keys panel) + const keysWidth = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--region-piano-key-width') + ) || 60; + const targetScrollLeft = playheadPixel - (container.clientWidth - keysWidth) / 2; + const clampedScrollLeft = Math.max( + 0, + Math.min(targetScrollLeft, container.scrollWidth - container.clientWidth) + ); + + container.scrollLeft = clampedScrollLeft; + + // Clear the request after handling + useProjectStore.setState({ pianoRollScrollRequest: null }); + }, [pianoRollScrollRequest]); + // Update --region-grid-beat-width when zoom changes; reset on unmount useEffect(() => { document.documentElement.style.setProperty('--region-grid-beat-width', `${40 * pianoRollZoom}px`); diff --git a/src/components/track/Track.css b/src/components/track/Track.css index 838f481..073cbbd 100644 --- a/src/components/track/Track.css +++ b/src/components/track/Track.css @@ -49,7 +49,7 @@ } .track-info { - width: 200px; + width: var(--track-info-panel-width); height: 120px; padding: 15px 5px 15px 15px; background-color: #2d2d2d; diff --git a/src/stores/projectStore.ts b/src/stores/projectStore.ts index 836f557..f7159a0 100644 --- a/src/stores/projectStore.ts +++ b/src/stores/projectStore.ts @@ -109,6 +109,12 @@ interface ProjectState { canRedo: boolean; undoDescription: string | null; redoDescription: string | null; + + // Cross-component scroll request state + requestMainContentScroll: (beatPosition: number) => void; + requestPianoRollScroll: (beatPosition: number) => void; + mainContentScrollRequest: number | null; + pianoRollScrollRequest: number | null; // Actions setProjectName: (name: string) => void; @@ -340,6 +346,10 @@ export const useProjectStore = create((set, get) => { recordingNotes: [], recordingOriginalPlayhead: 0, + // Initial cross-component scroll request state + mainContentScrollRequest: null, + pianoRollScrollRequest: null, + // Actions setProjectName: (name: string) => { try { @@ -793,6 +803,14 @@ export const useProjectStore = create((set, get) => { set({ autoScrollEnabled: enabled }); }, + requestMainContentScroll: (beatPosition: number) => { + set({ mainContentScrollRequest: beatPosition }); + }, + + requestPianoRollScroll: (beatPosition: number) => { + set({ pianoRollScrollRequest: beatPosition }); + }, + startPlaying: async () => { await KGCore.instance().startPlaying(); set({ isPlaying: true, autoScrollEnabled: true }); diff --git a/src/styles/variables.css b/src/styles/variables.css index a08337e..8d30d51 100644 --- a/src/styles/variables.css +++ b/src/styles/variables.css @@ -2,6 +2,7 @@ --time-signature-numerator: 4; --max-number-of-bars: 32; --track-grid-bar-width: 40px; + --track-info-panel-width: 200px; --region-piano-key-width: 60px; --region-piano-key-height: 20px; --region-grid-beat-width: 40px;