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:
@@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user