Skip to content
Merged
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
27 changes: 18 additions & 9 deletions .github/workflows/studio-drag-frames.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,29 @@ on:
ref:
description: Commit, branch or tag to measure
default: main
filter:
description: Case ids to run (a regular expression)
default: "^(move|resize|rotate|crop)-(none|tween)-px-r0-root-z100$"
grid:
description: The grid the ids come from (pr or full)
default: pr
pull_request:
paths:
- ".github/workflows/studio-drag-frames.yml"

jobs:
drag-frames:
name: "Studio: drag frames"
name: "Studio: drag frames (run ${{ matrix.run }})"
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
run: [1, 2, 3, 4, 5]
env:
BENCH_REF: ${{ inputs.ref || 'main' }}
BENCH_FILTER: ${{ inputs.filter || '^(move|resize|rotate|crop)-(none|tween)-px-r0-root-z100$' }}
BENCH_GRID: ${{ inputs.grid || 'pr' }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
Expand All @@ -40,17 +52,14 @@ jobs:
- run: bash scripts/ci/install-workspace-dependencies.sh
- run: bun run build
- run: node packages/cli/dist/cli.js browser ensure
- name: Run the drag cases five times
- name: Run the drag cases
working-directory: packages/studio
run: |
set -euo pipefail
echo "Measuring $(git rev-parse HEAD)"
for n in 1 2 3 4 5; do
bun tests/e2e/edit-accuracy/run.mjs --grid pr \
--filter '^(move|resize|rotate|crop)-(none|tween)-px-r0-root-z100$' \
--jobs 1 --port 5961 --cli "$GITHUB_WORKSPACE/packages/cli/dist/cli.js" \
--out "$RUNNER_TEMP/drag-frames/run-$n"
done
bun tests/e2e/edit-accuracy/run.mjs --grid "$BENCH_GRID" --filter "$BENCH_FILTER" \
--jobs 1 --port 5961 --cli "$GITHUB_WORKSPACE/packages/cli/dist/cli.js" \
--out "$RUNNER_TEMP/drag-frames/run-${{ matrix.run }}"
- name: Require a quiet box (the blank page dropped no frame)
run: |
# shellcheck disable=SC2016 # the backticks are JavaScript template strings
Expand All @@ -70,7 +79,7 @@ jobs:
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
if: always()
with:
name: studio-drag-frames
name: studio-drag-frames-${{ matrix.run }}
path: ${{ runner.temp }}/drag-frames
if-no-files-found: error
retention-days: 30
166 changes: 82 additions & 84 deletions packages/studio/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useOwnPreviewIframe, usePreviewIframeStore } from "./player/store/previewIframeStore";
import { buildProjectApiPath } from "./utils/projectRouting";
import { useState, useCallback, useRef, useMemo, useLayoutEffect } from "react";
import { useState, useCallback, useRef, useMemo } from "react";
import { useStableHandlers } from "./hooks/useStableHandlers";
import { useHistoryFlags, useToolbarSession } from "./hooks/useShellSlices";
import { useDismissingTabSetter, useRightPanelIntent } from "./hooks/useRightPanelIntents";
import { useRenderQueue } from "./components/renders/useRenderQueue";
import { usePlayerStore } from "./player";
Expand Down Expand Up @@ -46,10 +48,9 @@ import {
import type { DomEditSelection } from "./components/editor/domEditing";
import { StudioHeader } from "./components/StudioHeader";
import { useGestureCommit } from "./hooks/useGestureCommit";
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
import { StudioLeftPanels } from "./components/StudioLeftPanels";
import { useGestureTrailOverlay } from "./hooks/useGestureTrailOverlay";
import { EditorShell } from "./components/EditorShell";
import { StudioRightPanels } from "./components/StudioRightPanels";
import { useStudioSidePanels } from "./hooks/useStudioSidePanels";
import { TimelineToolbar } from "./components/TimelineToolbar";
import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
Expand Down Expand Up @@ -99,17 +100,18 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
rightCollapsed: initialUrlStateRef.current.rightCollapsed,
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
});
const editHistory = usePersistentEditHistory({ projectId });
const editHistory = useStableHandlers(usePersistentEditHistory({ projectId }), projectId);
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
const pendingTimelineEditPathRef = useRef(new Set<string>());
const isGestureRecordingRef = useRef(false);
const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), []);
const fileManager = useFileManager({
const fileManagerResult = useFileManager({
projectId,
showToast,
recordEdit: editHistory.recordEdit,
setRefreshKey,
});
const fileManager = useStableHandlers(fileManagerResult, projectId);
const masterCompPath = useMemo(
() => resolveMasterCompositionPath(fileManager.compositions),
[fileManager.compositions],
Expand All @@ -134,8 +136,8 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
);
const activeCompPathRef = useRef(activeCompPath);
activeCompPathRef.current = activeCompPath;
const renderQueue = useRenderQueue(projectId, activeCompPathRef);
const previewPersistence = usePreviewPersistence({
const renderQueue = useStableHandlers(useRenderQueue(projectId, activeCompPathRef), projectId);
const previewPersistenceResult = usePreviewPersistence({
showToast,
readOptionalProjectFile: fileManager.readOptionalProjectFile,
writeProjectFile: fileManager.writeProjectFile,
Expand All @@ -144,6 +146,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
activeCompPathRef,
reloadPreview: () => setRefreshKey((k) => k + 1),
});
const previewPersistence = useStableHandlers(previewPersistenceResult, projectId);
const externalFileChanges = useStudioExternalFileChanges({
projectId,
activeCompPath,
Expand Down Expand Up @@ -184,15 +187,9 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
} = useTimelineAddAtPlayhead(
timelineEditing.handleTimelineAssetDrop,
timelineEditing.handleTimelineCompositionDrop,
projectId,
);
const {
activeBlockParams,
setActiveBlockParams,
handleAddBlock,
handleTimelineBlockDrop,
handleAddMediaOverlay,
handlePreviewBlockDrop,
} = useBlockHandlers({
const blockHandlersResult = useBlockHandlers({
projectId,
blockCtxDeps: {
activeCompPath,
Expand All @@ -209,6 +206,14 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
setRightCollapsed: panelLayout.setRightCollapsed,
setRightPanelTab: panelLayout.setRightPanelTab,
});
const {
activeBlockParams,
setActiveBlockParams,
handleAddBlock,
handleTimelineBlockDrop,
handleAddMediaOverlay,
handlePreviewBlockDrop,
} = useStableHandlers(blockHandlersResult, projectId);
const dismissBlockParams = useCallback(() => setActiveBlockParams(null), [setActiveBlockParams]);
const setRightPanelTab = useDismissingTabSetter(panelLayout.setRightPanelTab, dismissBlockParams);
const layout = useMemo(
Expand Down Expand Up @@ -237,6 +242,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
});
const appHotkeys = useAppHotkeys({
projectId,
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
handleLinkEdit: timelineEditing.handleLinkEdit,
handleTimelineElementDeleteOnly: timelineEditing.handleTimelineElementDeleteOnly,
Expand Down Expand Up @@ -269,7 +275,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
onToggleRecording: () => handleToggleRecordingRef.current(),
readOnlyPreview,
});
const domEditSession = useDomEditSession({
const domEditSessionResult = useDomEditSession({
projectId,
activeCompPath,
compIdToSrc,
Expand Down Expand Up @@ -309,6 +315,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
readOnlyPreview,
});
const domEditSession = useStableHandlers(domEditSessionResult, projectId);
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
const { handleDomZIndexReorderCommit: zCommit, handleDomEditElementDelete: del } = domEditSession;
handleDomZIndexReorderCommitRef.current = trackedStudioEdit(zCommit);
Expand Down Expand Up @@ -342,13 +349,14 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
effectiveTimelineDuration,
});
const compositionDimensions = useCompositionDimensions(previewIframeRef);
const lint = useLintModal(projectId, refreshKey);
const frameCapture = useFrameCapture({
const lint = useStableHandlers(useLintModal(projectId, refreshKey), projectId);
const frameCaptureResult = useFrameCapture({
projectId,
activeCompPath,
showToast,
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
});
const frameCapture = useStableHandlers(frameCaptureResult, projectId);
const {
consoleErrors,
setConsoleErrors,
Expand All @@ -359,21 +367,19 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
const domEditSessionRef = useRef(domEditSession);
domEditSessionRef.current = domEditSession;
const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
projectId,
domEditSessionRef,
previewIframeRef,
showToast,
isGestureRecordingRef,
readOnlyPreview,
});
handleToggleRecordingRef.current = handleToggleRecording;
const canvasRectRef = useRef<DOMRect | null>(null);
useLayoutEffect(() => {
if (gestureState !== "recording" || !previewIframe) {
canvasRectRef.current = null;
return;
}
canvasRectRef.current = previewIframe.getBoundingClientRect();
}, [gestureState, previewIframe]);
const gestureOverlay = useGestureTrailOverlay(
{ gestureState, gestureRecording },
previewIframe,
compositionDimensions,
);
const handlePreviewIframeRef = useCallback(
(iframe: HTMLIFrameElement | null) => {
previewIframeRef.current = iframe;
Expand Down Expand Up @@ -415,6 +421,8 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
setRightPanelTab,
initialState: initialUrlStateRef.current,
});
const historyFlags = useHistoryFlags(editHistory);
const toolbarSession = useToolbarSession(domEditSession);
const studioCtxValue = buildStudioContextValue({
projectId: projectId!,
activeCompPath,
Expand All @@ -427,7 +435,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
setRefreshKey,
timelineElements,
isPlaying,
editHistory,
editHistory: historyFlags,
handleUndo: appHotkeys.handleUndo,
handleRedo: appHotkeys.handleRedo,
renderQueue,
Expand All @@ -441,11 +449,51 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
const timelineToolbar = useMemo(
() => (
<TimelineToolbar
domEditSession={domEditSession}
domEditSession={toolbarSession}
onSplitElement={timelineEditing.handleTimelineElementSplit}
/>
),
[domEditSession, timelineEditing.handleTimelineElementSplit],
[toolbarSession, timelineEditing.handleTimelineElementSplit],
);
const handleExport = useCallback(() => {
void previewPersistence
.waitForPendingDomEditSaves()
.then(() => renderQueue.startRender(undefined));
}, [previewPersistence, renderQueue]);
const clearConsoleErrors = useCallback(() => setConsoleErrors(null), [setConsoleErrors]);
const panels = useStudioSidePanels(
{
onSelectComposition: handleSelectComposition,
onAddBlock: handleAddBlock,
onPreviewBlock: setBlockPreview,
onLint: lint.handleLint,
linting: lint.linting,
lintFindingCount: lint.lintFindingCount,
lintFindingsByFile: lint.findingsByFile,
lintHasError: lint.hasLintError,
onAddAssetToTimeline: handleAddAssetAtPlayhead,
onAddCompositionToTimeline: handleAddCompositionAtPlayhead,
},
{
activeBlockParams,
onDismissBlockParams: dismissBlockParams,
onCloseBlockParams: () => {
setActiveBlockParams(null);
panelLayout.setRightPanelTab("design");
},
recordingState: gestureState,
recordingDuration: gestureRecording.recordingDuration,
onToggleRecording: handleToggleRecording,
sdkSession: sdkHandle.session,
publishSdkSession: sdkHandle.publish,
forceReloadSdkSession: sdkHandle.forceReload,
reloadPreview,
recordEdit: editHistory.recordEdit,
onToggleElementHidden: timelineEditing.handleToggleElementHidden,
onAutoGroupCarveSources: timelineEditing.handleAutoGroupCarveSources,
onAddMediaOverlay: handleAddMediaOverlay,
},
projectId,
);
if (resolving || waitingForServer || !projectId)
return <StudioSplash waiting={waitingForServer} />;
Expand All @@ -468,12 +516,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
capturing={frameCapture.capturing}
inspectorButtonActive={inspectorButtonActive}
inspectorPanelActive={inspectorPanelActive}
onExport={() => {
void (async () => {
await previewPersistence.waitForPendingDomEditSaves();
await renderQueue.startRender(undefined);
})();
}}
onExport={handleExport}
/>
{previewPersistence.domEditSaveQueuePaused && !externalFileChanges.blocked && (
<SaveQueuePausedBanner
Expand All @@ -491,41 +534,7 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
<EditorShell
readOnlyPreview={readOnlyPreview}
readOnlyPreviewReason={readOnlyPreviewReason}
panels={
<>
<StudioLeftPanels
onSelectComposition={handleSelectComposition}
onAddBlock={handleAddBlock}
onPreviewBlock={setBlockPreview}
onLint={lint.handleLint}
linting={lint.linting}
lintFindingCount={lint.lintFindingCount}
lintFindingsByFile={lint.findingsByFile}
lintHasError={lint.hasLintError}
onAddAssetToTimeline={handleAddAssetAtPlayhead}
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
/>
<StudioRightPanels
activeBlockParams={activeBlockParams}
onDismissBlockParams={dismissBlockParams}
onCloseBlockParams={() => {
setActiveBlockParams(null);
panelLayout.setRightPanelTab("design");
}}
recordingState={gestureState}
recordingDuration={gestureRecording.recordingDuration}
onToggleRecording={handleToggleRecording}
sdkSession={sdkHandle.session}
publishSdkSession={sdkHandle.publish}
forceReloadSdkSession={sdkHandle.forceReload}
reloadPreview={reloadPreview}
recordEdit={editHistory.recordEdit}
onToggleElementHidden={timelineEditing.handleToggleElementHidden}
onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
onAddMediaOverlay={handleAddMediaOverlay}
/>
</>
}
panels={panels}
timelineToolbar={timelineToolbar}
renderClipContent={renderClipContent}
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
Expand Down Expand Up @@ -561,26 +570,15 @@ export function StudioApp({ readOnlyPreview = false, readOnlyPreviewReason }: St
recordingState={gestureState}
onToggleRecording={handleToggleRecording}
blockPreview={blockPreview}
gestureOverlay={
gestureState === "recording" && previewIframe ? (
<GestureTrailOverlay
samples={gestureRecording.samplesRef.current}
sampleCount={gestureRecording.samplesRef.current.length}
trail={gestureRecording.trailRef.current}
canvasRect={canvasRectRef.current!}
compositionSize={compositionDimensions ?? undefined}
mode="recording"
/>
) : undefined
}
gestureOverlay={gestureOverlay}
/>
<StudioOverlays
projectId={projectId}
projectDir={fileManager.projectDir}
lintModal={lint.lintModal}
closeLintModal={lint.closeLintModal}
consoleErrors={consoleErrors}
clearConsoleErrors={() => setConsoleErrors(null)}
clearConsoleErrors={clearConsoleErrors}
domEditSession={domEditSession}
activeCompPath={activeCompPath}
toasts={toasts}
Expand Down
Loading
Loading