feat: add intuitive drag-to-select on waveforms

Improved selection UX to match professional DAWs:
- Drag directly on waveform to create selections (no modifier keys needed)
- Click without dragging seeks playhead and clears selection
- 3-pixel drag threshold prevents accidental selections
- Fixed variable name conflict with existing file drag-and-drop feature

Users can now naturally drag across waveforms to select regions for
editing, providing a more intuitive workflow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 14:25:49 +01:00
parent 74879a42cf
commit 1dc0604635

View File

@@ -68,6 +68,8 @@ export function Track({
// Selection state // Selection state
const [isSelecting, setIsSelecting] = React.useState(false); const [isSelecting, setIsSelecting] = React.useState(false);
const [selectionStart, setSelectionStart] = React.useState<number | null>(null); const [selectionStart, setSelectionStart] = React.useState<number | null>(null);
const [isSelectingByDrag, setIsSelectingByDrag] = React.useState(false);
const [dragStartPos, setDragStartPos] = React.useState<{ x: number; y: number } | null>(null);
const handleNameClick = () => { const handleNameClick = () => {
setIsEditingName(true); setIsEditingName(true);
@@ -224,41 +226,66 @@ export function Track({
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const clickTime = (x / rect.width) * duration; const clickTime = (x / rect.width) * duration;
// Shift+drag to create selection // Store drag start position
if (e.shiftKey) { setDragStartPos({ x: e.clientX, y: e.clientY });
setIsSelecting(true); setIsSelectingByDrag(false);
setSelectionStart(clickTime);
onSelectionChange?.({ start: clickTime, end: clickTime }); // Start selection immediately (will be used if user drags)
} else { setIsSelecting(true);
// Regular click clears selection and seeks setSelectionStart(clickTime);
onSelectionChange?.(null);
if (onSeek) {
onSeek(clickTime);
}
}
}; };
const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => { const handleCanvasMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!isSelecting || selectionStart === null || !duration) return; if (!isSelecting || selectionStart === null || !duration || !dragStartPos) return;
const rect = e.currentTarget.getBoundingClientRect(); const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left; const x = e.clientX - rect.left;
const currentTime = (x / rect.width) * duration; const currentTime = (x / rect.width) * duration;
// Clamp to valid time range // Check if user has moved enough to be considered dragging (threshold: 3 pixels)
const clampedTime = Math.max(0, Math.min(duration, currentTime)); const dragDistance = Math.sqrt(
Math.pow(e.clientX - dragStartPos.x, 2) + Math.pow(e.clientY - dragStartPos.y, 2)
);
// Update selection (ensure start < end) if (dragDistance > 3) {
const start = Math.min(selectionStart, clampedTime); setIsSelectingByDrag(true);
const end = Math.max(selectionStart, clampedTime); }
onSelectionChange?.({ start, end }); // If dragging, update selection
if (isSelectingByDrag || dragDistance > 3) {
// Clamp to valid time range
const clampedTime = Math.max(0, Math.min(duration, currentTime));
// Update selection (ensure start < end)
const start = Math.min(selectionStart, clampedTime);
const end = Math.max(selectionStart, clampedTime);
onSelectionChange?.({ start, end });
}
}; };
const handleCanvasMouseUp = () => { const handleCanvasMouseUp = (e: React.MouseEvent<HTMLCanvasElement>) => {
if (!duration) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const clickTime = (x / rect.width) * duration;
// If user didn't drag (just clicked), clear selection and seek
if (!isSelectingByDrag) {
onSelectionChange?.(null);
if (onSeek) {
onSeek(clickTime);
}
}
// Reset drag state
setIsSelecting(false); setIsSelecting(false);
setIsSelectingByDrag(false);
setDragStartPos(null);
}; };
// Handle mouse leaving canvas during selection // Handle mouse leaving canvas during selection
@@ -266,6 +293,8 @@ export function Track({
const handleGlobalMouseUp = () => { const handleGlobalMouseUp = () => {
if (isSelecting) { if (isSelecting) {
setIsSelecting(false); setIsSelecting(false);
setIsSelectingByDrag(false);
setDragStartPos(null);
} }
}; };