fix(timeline): make horizontal scroll easier

This commit is contained in:
wiiiii123
2026-05-26 23:29:14 +07:00
parent 7673f12584
commit 035d4a64bd
5 changed files with 111 additions and 15 deletions
@@ -10,13 +10,13 @@ export function KeyboardShortcutsHelp() {
const t = useScopedT("editor");
const [scrollLabels, setScrollLabels] = useState({
pan: "Shift + Ctrl + Scroll",
pan: "Shift + Scroll",
zoom: "Ctrl + Scroll",
});
useEffect(() => {
Promise.all([
formatShortcut(["shift", "mod", "Scroll"]),
formatShortcut(["shift", "Scroll"]),
formatShortcut(["mod", "Scroll"]),
]).then(([pan, zoom]) => setScrollLabels({ pan, zoom }));
}, []);
+2 -2
View File
@@ -149,13 +149,13 @@ export function KeyboardShortcutsDialog({
const { shortcuts, isMac, openConfig } = useShortcuts();
const t = useScopedT("editor");
const [scrollLabels, setScrollLabels] = useState({
pan: "Shift + Ctrl + Scroll",
pan: "Shift + Scroll",
zoom: "Ctrl + Scroll",
});
useEffect(() => {
Promise.all([
formatShortcut(["shift", "mod", "Scroll"]),
formatShortcut(["shift", "Scroll"]),
formatShortcut(["mod", "Scroll"]),
]).then(([pan, zoom]) => setScrollLabels({ pan, zoom }));
}, []);
@@ -0,0 +1,57 @@
import { describe, expect, it } from "vitest";
import { resolveTimelineWheelPanDeltaPx } from "./useTimelineRange";
describe("resolveTimelineWheelPanDeltaPx", () => {
it("uses trackpad horizontal wheel movement for timeline panning", () => {
expect(
resolveTimelineWheelPanDeltaPx({
deltaX: 24,
deltaY: 0,
deltaMode: 0,
}),
).toBe(24);
});
it("uses shifted vertical wheel movement for timeline panning", () => {
expect(
resolveTimelineWheelPanDeltaPx({
deltaX: 0,
deltaY: 3,
deltaMode: 1,
shiftKey: true,
}),
).toBe(48);
});
it("keeps ctrl wheel available for timeline zoom unless shift is also held", () => {
expect(
resolveTimelineWheelPanDeltaPx({
deltaX: 0,
deltaY: 3,
deltaMode: 1,
ctrlKey: true,
}),
).toBe(0);
expect(
resolveTimelineWheelPanDeltaPx({
deltaX: 0,
deltaY: 3,
deltaMode: 1,
ctrlKey: true,
shiftKey: true,
}),
).toBe(48);
});
it("uses regular wheel movement when the timeline has no vertical overflow", () => {
expect(
resolveTimelineWheelPanDeltaPx({
deltaX: 0,
deltaY: 20,
deltaMode: 0,
canScrollVertically: false,
}),
).toBe(20);
});
});
@@ -7,6 +7,40 @@ interface UseTimelineRangeParams {
timelineContainerRef: RefObject<HTMLDivElement>;
}
export interface TimelineWheelPanDeltaInput {
deltaX: number;
deltaY: number;
deltaMode: number;
shiftKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
canScrollVertically?: boolean;
}
export function resolveTimelineWheelPanDeltaPx({
deltaX,
deltaY,
deltaMode,
shiftKey = false,
ctrlKey = false,
metaKey = false,
canScrollVertically = true,
}: TimelineWheelPanDeltaInput) {
if ((ctrlKey || metaKey) && !shiftKey) {
return 0;
}
if (Math.abs(deltaX) > 0) {
return normalizeWheelDeltaToPixels(deltaX, deltaMode);
}
if ((shiftKey || !canScrollVertically) && Math.abs(deltaY) > 0) {
return normalizeWheelDeltaToPixels(deltaY, deltaMode);
}
return 0;
}
export function useTimelineRange({ totalMs, timelineContainerRef }: UseTimelineRangeParams) {
const [range, setRange] = useState<Range>(() => createInitialRange(totalMs));
@@ -42,29 +76,34 @@ export function useTimelineRange({ totalMs, timelineContainerRef }: UseTimelineR
const handleTimelineWheel = useCallback(
(event: WheelEvent<HTMLDivElement>) => {
if (event.ctrlKey || event.metaKey || totalMs <= 0) {
if (((event.ctrlKey || event.metaKey) && !event.shiftKey) || totalMs <= 0) {
return;
}
const rawHorizontalDelta =
Math.abs(event.deltaX) > 0
? event.deltaX
: event.shiftKey && Math.abs(event.deltaY) > 0
? event.deltaY
: 0;
const container = timelineContainerRef.current;
const horizontalDeltaPx = resolveTimelineWheelPanDeltaPx({
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaMode: event.deltaMode,
shiftKey: event.shiftKey,
ctrlKey: event.ctrlKey,
metaKey: event.metaKey,
canScrollVertically: container
? container.scrollHeight > container.clientHeight + 1
: true,
});
if (rawHorizontalDelta === 0) {
if (horizontalDeltaPx === 0) {
return;
}
const containerWidth = timelineContainerRef.current?.clientWidth ?? 0;
const containerWidth = container?.clientWidth ?? 0;
const visibleRangeMs = clampedRange.end - clampedRange.start;
if (containerWidth <= 0 || visibleRangeMs <= 0) {
return;
}
event.preventDefault();
const horizontalDeltaPx = normalizeWheelDeltaToPixels(rawHorizontalDelta, event.deltaMode);
const deltaMs = (horizontalDeltaPx / containerWidth) * visibleRangeMs;
panTimelineRange(deltaMs);
},
+1 -1
View File
@@ -37,7 +37,7 @@ export const FIXED_SHORTCUTS: FixedShortcut[] = [
display: "Del / ⌫",
bindings: [{ key: "delete" }, { key: "backspace" }],
},
{ label: "Pan Timeline", display: "Shift + Ctrl + Scroll", bindings: [] },
{ label: "Pan Timeline", display: "Shift + Scroll", bindings: [] },
{ label: "Zoom Timeline", display: "Ctrl + Scroll", bindings: [] },
];