Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface ElementSelectionApi {
getSelected: () => readonly ElementRef[];
isSelected: (ref: ElementRef) => boolean;
select: (ref: ElementRef) => void;
selectRange: (args: {
anchor?: ElementRef | null;
target: ElementRef;
}) => readonly ElementRef[];
handleClick: (args: ElementRef & { isMultiKey: boolean }) => void;
clearKeyframeSelection: () => void;
}
Expand Down Expand Up @@ -295,6 +299,7 @@ export class ElementInteractionController {
// has already returned to idle, so the "was this a drag?" answer must
// outlive the session. Reset on the next mousedown.
private lastGestureWasDrag = false;
private elementSelectionAnchor: ElementRef | null = null;

private readonly subscribers = new Set<() => void>();
private readonly depsRef: ElementInteractionDepsRef;
Expand Down Expand Up @@ -372,13 +377,29 @@ export class ElementInteractionController {

const ref = { trackId: track.id, elementId: element.id };

if (event.metaKey || event.ctrlKey || event.shiftKey) {
let rangeSelection: readonly ElementRef[] | null = null;
if (event.shiftKey) {
const anchor =
this.elementSelectionAnchor?.trackId === ref.trackId
? this.elementSelectionAnchor
: null;
rangeSelection = this.deps.selection.selectRange({
anchor: anchor ?? ref,
target: ref,
});

if (!anchor) {
this.elementSelectionAnchor = ref;
}
} else if (event.metaKey || event.ctrlKey) {
this.deps.selection.handleClick({ ...ref, isMultiKey: true });
}

const selectedElements = this.deps.selection.isSelected(ref)
? this.deps.selection.getSelected()
: [ref];
const selectedElements =
rangeSelection ??
(this.deps.selection.isSelected(ref)
? this.deps.selection.getSelected()
: [ref]);

this.session = {
kind: "pending",
Expand Down Expand Up @@ -423,9 +444,11 @@ export class ElementInteractionController {
this.deps.selection.getSelected().length > 1
) {
this.deps.selection.select(ref);
this.elementSelectionAnchor = ref;
return;
}

this.elementSelectionAnchor = ref;
this.deps.selection.clearKeyframeSelection();
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function useElementInteraction({
getSelected: () => selection.selectedElements,
isSelected: selection.isElementSelected,
select: selection.selectElement,
selectRange: selection.selectElementRange,
handleClick: selection.handleElementClick,
clearKeyframeSelection: () => editor.selection.clearKeyframeSelection(),
},
Expand Down
56 changes: 56 additions & 0 deletions apps/web/src/timeline/hooks/element/use-element-selection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback } from "react";
import { useEditor } from "@/editor/use-editor";
import { findTrackInSceneTracks } from "@/timeline/track-element-update";
import type { ElementRef } from "@/timeline/types";

export function useElementSelection() {
Expand Down Expand Up @@ -101,6 +102,60 @@ export function useElementSelection() {
[selectedElements, editor],
);

/**
* Handles Shift-click range selection. Selects all clips between the anchor
* and target when both are on the same track; otherwise selects only target.
*/
const selectElementRange = useCallback(
({
anchor,
target,
}: {
anchor?: ElementRef | null;
target: ElementRef;
}) => {
const setSelection = (elements: ElementRef[]) => {
editor.selection.setSelectedElements({ elements });
return elements;
};

const track = findTrackInSceneTracks({
tracks: editor.scenes.getActiveScene().tracks,
trackId: target.trackId,
});
if (!track || anchor?.trackId !== target.trackId) {
return setSelection([target]);
}

const orderedElements = track.elements
.map((element, index) => ({ element, index }))
.sort((a, b) =>
a.element.startTime === b.element.startTime
? a.index - b.index
: a.element.startTime - b.element.startTime,
);
const anchorIndex = orderedElements.findIndex(
({ element }) => element.id === anchor.elementId,
);
const targetIndex = orderedElements.findIndex(
({ element }) => element.id === target.elementId,
);

if (anchorIndex === -1 || targetIndex === -1) {
return setSelection([target]);
}

const start = Math.min(anchorIndex, targetIndex);
const end = Math.max(anchorIndex, targetIndex);
return setSelection(
orderedElements.slice(start, end + 1).map(({ element }) => ({
trackId: target.trackId,
elementId: element.id,
})),
);
},
[editor],
);

/**
* Handles click interaction on an element.
Expand Down Expand Up @@ -128,6 +183,7 @@ export function useElementSelection() {
selectElement,
setElementSelection,
mergeElementsIntoSelection,
selectElementRange,
addElementToSelection,
removeElementFromSelection,
toggleElementSelection,
Expand Down