import { getAudioContext, webaudioOutput } from '@strudel.cycles/webaudio'; import React, { useLayoutEffect, useMemo, useRef, useState, useCallback, useEffect } from 'react'; import { useInView } from 'react-hook-inview'; import 'tailwindcss/tailwind.css'; import cx from '../cx'; import useHighlighting from '../hooks/useHighlighting.mjs'; import useStrudel from '../hooks/useStrudel.mjs'; import CodeMirror6, { flash } from './CodeMirror6'; import { Icon } from './Icon'; import styles from './MiniRepl.module.css'; import './style.css'; import { logger } from '@strudel.cycles/core'; const getTime = () => getAudioContext().currentTime; export function MiniRepl({ tune, hideOutsideView = false, enableKeyboard, drawTime, punchcard, canvasHeight = 200 }) { drawTime = drawTime || (punchcard ? [0, 4] : undefined); const evalOnMount = !!drawTime; const drawContext = useCallback( !!drawTime ? (canvasId) => document.querySelector('#' + canvasId)?.getContext('2d') : null, [drawTime], ); const { code, setCode, evaluate, activateCode, error, isDirty, activeCode, pattern, started, scheduler, togglePlay, stop, canvasId, id: replId, } = useStrudel({ initialCode: tune, defaultOutput: webaudioOutput, editPattern: (pat) => (punchcard ? pat.punchcard() : pat), getTime, evalOnMount, drawContext, drawTime, }); const [view, setView] = useState(); const [ref, isVisible] = useInView({ threshold: 0.01, }); const wasVisible = useRef(); const show = useMemo(() => { if (isVisible || !hideOutsideView) { wasVisible.current = true; } return isVisible || wasVisible.current; }, [isVisible, hideOutsideView]); useHighlighting({ view, pattern, active: started && !activeCode?.includes('strudel disable-highlighting'), getTime: () => scheduler.now(), }); // keyboard shortcuts useKeydown( useCallback( async (e) => { if (view?.hasFocus) { if (e.ctrlKey || e.altKey) { if (e.code === 'Enter') { e.preventDefault(); flash(view); await activateCode(); } else if (e.code === 'Period') { stop(); e.preventDefault(); } } } }, [activateCode, stop, view], ), ); // set active pattern on ctrl+enter useLayoutEffect(() => { if (enableKeyboard) { const handleKeyPress = async (e) => { if (e.ctrlKey || e.altKey) { if (e.code === 'Enter') { e.preventDefault(); flash(view); await activateCode(); } else if (e.code === 'Period') { stop(); e.preventDefault(); } } }; window.addEventListener('keydown', handleKeyPress, true); return () => window.removeEventListener('keydown', handleKeyPress, true); } }, [enableKeyboard, pattern, code, evaluate, stop, view]); const [log, setLog] = useState([]); useLogger( useCallback((e) => { const { data } = e.detail; const logId = data?.hap?.context?.id; // const logId = data?.pattern?.meta?.id; if (logId === replId) { setLog((l) => { return l.concat([e.detail]).slice(-10); }); } }, []), ); return (
{error &&
{error.message}
}
{show && }
{drawTime && ( { if (el && el.width !== el.clientWidth) { el.width = el.clientWidth; } }} > )} {!!log.length && (
{log.map(({ message }, i) => (
{message}
))}
)}
); } // TODO: dedupe function useLogger(onTrigger) { useEvent(logger.key, onTrigger); } // TODO: dedupe function useEvent(name, onTrigger, useCapture = false) { useEffect(() => { document.addEventListener(name, onTrigger, useCapture); return () => { document.removeEventListener(name, onTrigger, useCapture); }; }, [onTrigger]); } // TODO: dedupe function useKeydown(onTrigger) { useEvent('keydown', onTrigger, true); }