diff --git a/packages/web/src/hooks/use-strudel-codemirror-extensions.ts b/packages/web/src/hooks/use-strudel-codemirror-extensions.ts new file mode 100644 index 00000000..0a4e1fcd --- /dev/null +++ b/packages/web/src/hooks/use-strudel-codemirror-extensions.ts @@ -0,0 +1,31 @@ +import { Session } from "@flok-editor/session"; +import { + highlightMiniLocations, + updateMiniLocations, +} from "@strudel/codemirror"; +import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { useCallback } from "react"; +import { useAnimationFrame } from "./use-animation-frame"; +import { forEachDocumentContext } from "@/lib/utils"; + +export function useStrudelCodemirrorExtensions( + session: Session | null, + editorRefs: React.RefObject[] +) { + useAnimationFrame( + useCallback(() => { + if (!session) return; + + forEachDocumentContext( + (ctx, editor) => { + const view = editor?.view; + if (!view) return; + updateMiniLocations(view, ctx.miniLocations || []); + highlightMiniLocations(view, ctx.phase || 0, ctx.haps || []); + }, + session, + editorRefs + ); + }, [session, editorRefs]) + ); +} diff --git a/packages/web/src/lib/strudel-wrapper.ts b/packages/web/src/lib/strudel-wrapper.ts index faf178e3..8bcd36c7 100644 --- a/packages/web/src/lib/strudel-wrapper.ts +++ b/packages/web/src/lib/strudel-wrapper.ts @@ -1,4 +1,4 @@ -import type { EvalMessage, Session } from "@flok-editor/session"; +import type { EvalMessage } from "@flok-editor/session"; import { Framer, Pattern, @@ -18,15 +18,12 @@ import { samples, webaudioOutput, } from "@strudel/webaudio"; -import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; +import { updateDocumentsContext } from "./utils"; export type ErrorHandler = (error: string) => void; controls.createParam("docId"); -const getDocumentIndex = (docId: string, session: Session | null) => - session?.getDocuments().findIndex((d) => d.id === docId) ?? -1; - export class StrudelWrapper { initialized: boolean = false; @@ -34,26 +31,18 @@ export class StrudelWrapper { protected _onWarning: ErrorHandler; protected _repl: any; protected _docPatterns: any; - protected _editorRefs: React.RefObject[]; - protected _session: Session | null; protected framer?: any; constructor({ onError, onWarning, - editorRefs, - session, }: { onError: ErrorHandler; onWarning: ErrorHandler; - editorRefs: React.RefObject[]; - session: Session | null; }) { this._docPatterns = {}; this._onError = onError || (() => {}); this._onWarning = onWarning || (() => {}); - this._editorRefs = editorRefs; - this._session = session; } async importModules() { @@ -92,9 +81,6 @@ export class StrudelWrapper { lastFrame = phase; return; } - if (!this._editorRefs) { - return; - } if (!this._repl.scheduler.pattern) { return; } @@ -109,19 +95,14 @@ export class StrudelWrapper { ); // iterate over each strudel doc Object.keys(this._docPatterns).forEach((docId: any) => { - const index = getDocumentIndex(docId, this._session); - const editorRef = this._editorRefs?.[index]; - const view = editorRef?.current?.view; - if (!view) return; // filter out haps belonging to this document (docId is set in tryEval) const haps = currentFrame.filter((h: any) => h.value.docId === docId); // update codemirror view to highlight this frame's haps - window.parent.phase = phase; - window.parent.haps = haps; + updateDocumentsContext(docId, { haps, phase }); }); }, (err: any) => { - console.error("strudel draw error", err); + console.error("[strudel] draw error", err); } ); @@ -130,12 +111,9 @@ export class StrudelWrapper { afterEval: (options: any) => { // assumes docId is injected at end end as a comment const docId = options.code.split("//").slice(-1)[0]; - const index = getDocumentIndex(docId, this._session); - const editorRef = this._editorRefs?.[index]; - if (editorRef?.current) { - const miniLocations = options.meta?.miniLocations; - window.parent.miniLocations = miniLocations; - } + if (!docId) return; + const miniLocations = options.meta?.miniLocations; + updateDocumentsContext(docId, { miniLocations }); }, beforeEval: () => {}, onSchedulerError: (e: unknown) => this._onError(`${e}`), @@ -144,16 +122,22 @@ export class StrudelWrapper { transpiler, }); - this.framer.start(); // TODO: when to start stop? + this.framer.start(); // For some reason, we need to make a no-op evaluation ("silence") to make // sure everything is loaded correctly. - const pattern = await this._repl.evaluate(`silence`); + const pattern = await this._repl.evaluate(`silence//`); await this._repl.scheduler.setPattern(pattern, true); this.initialized = true; } + async dispose() { + if (this.framer) { + this.framer.stop(); + } + } + async tryEval(msg: EvalMessage) { if (!this.initialized) await this.initialize(); try { diff --git a/packages/web/src/lib/utils.ts b/packages/web/src/lib/utils.ts index cf0d76ab..309a1be5 100644 --- a/packages/web/src/lib/utils.ts +++ b/packages/web/src/lib/utils.ts @@ -1,10 +1,12 @@ +import { type Session } from "@flok-editor/session"; +import { ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; import { - uniqueNamesGenerator, adjectives, - colors, animals, + colors, + uniqueNamesGenerator, } from "unique-names-generator"; import { v4 as uuidv4 } from "uuid"; @@ -93,3 +95,28 @@ export function sendToast( "*" ); } + +export function updateDocumentsContext(docId: string, context: object) { + if (typeof window.parent.documentsContext === "undefined") { + window.parent.documentsContext = {}; + } + const prevContext = window.parent.documentsContext[docId] || {}; + window.parent.documentsContext[docId] = { ...prevContext, ...context }; +} + +export function forEachDocumentContext( + callback: (context: any, editor: ReactCodeMirrorRef | null) => void, + session: Session, + editorRefs: React.RefObject[] +) { + const documentsContext = window.documentsContext || {}; + for (const docId in documentsContext) { + const context = documentsContext[docId]; + const index = getDocumentIndex(docId, session); + const editor = editorRefs[index]?.current; + callback(context, editor); + } +} + +export const getDocumentIndex = (docId: string, session: Session | null) => + session?.getDocuments().findIndex((d) => d.id === docId) ?? -1; diff --git a/packages/web/src/routes/frames/strudel.tsx b/packages/web/src/routes/frames/strudel.tsx index e9815412..19ed3a2c 100644 --- a/packages/web/src/routes/frames/strudel.tsx +++ b/packages/web/src/routes/frames/strudel.tsx @@ -16,8 +16,6 @@ export function Component() { onWarning: (msg) => { sendToast("warning", "Strudel warning", msg); }, - session: window.parent.session, - editorRefs: window.parent.editorRefs, }); await instance.importModules(); diff --git a/packages/web/src/routes/session.tsx b/packages/web/src/routes/session.tsx index 42b104a1..8eb9eb58 100644 --- a/packages/web/src/routes/session.tsx +++ b/packages/web/src/routes/session.tsx @@ -13,10 +13,10 @@ import { Toaster } from "@/components/ui/toaster"; import UsernameDialog from "@/components/username-dialog"; import { WebTargetIframe } from "@/components/web-target-iframe"; import { WelcomeDialog } from "@/components/welcome-dialog"; -import { useAnimationFrame } from "@/hooks/use-animation-frame"; import { useHash } from "@/hooks/use-hash"; import { useQuery } from "@/hooks/use-query"; import { useShortcut } from "@/hooks/use-shortcut"; +import { useStrudelCodemirrorExtensions } from "@/hooks/use-strudel-codemirror-extensions"; import { useToast } from "@/hooks/use-toast"; import { cn, @@ -33,10 +33,6 @@ import { webTargets, } from "@/settings.json"; import { Session, type Document } from "@flok-editor/session"; -import { - highlightMiniLocations, - updateMiniLocations, -} from "@strudel/codemirror"; import { type ReactCodeMirrorRef } from "@uiw/react-codemirror"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Helmet } from "react-helmet-async"; @@ -48,11 +44,7 @@ import { declare global { interface Window { - session: Session | null; - editorRefs: React.RefObject[]; - miniLocations: any; - phase: any; - haps: any; + documentsContext: { [docId: string]: any }; } } @@ -108,20 +100,7 @@ export function Component() { useRef(null) ); - useEffect(() => { - window.editorRefs = editorRefs; - }, [editorRefs]); - - useAnimationFrame( - useCallback(() => { - editorRefs.forEach((editorRef) => { - const view = editorRef?.current?.view; - if (!view) return; - updateMiniLocations(view, window.miniLocations || []); - highlightMiniLocations(view, window.phase || 0, window.haps || []); - }); - }, [editorRefs]) - ); + useStrudelCodemirrorExtensions(session, editorRefs); const { toast: _toast } = useToast(); const hideErrors = !!query.get("hideErrors"); @@ -158,8 +137,6 @@ export function Component() { isSecure, }); - window.session = newSession; - // Default documents newSession.on("sync", (protocol: string) => { setSyncState(newSession.wsConnected ? "synced" : "partiallySynced");