From d6cc3a419be866013bc7c3bb86088cbcb14602d6 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Tue, 26 Jul 2022 23:00:20 +0400 Subject: [PATCH 1/6] remove toggle animation --- packages/components/src/components/ui/Toggle.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/components/src/components/ui/Toggle.tsx b/packages/components/src/components/ui/Toggle.tsx index 90e73e08..3295bda0 100644 --- a/packages/components/src/components/ui/Toggle.tsx +++ b/packages/components/src/components/ui/Toggle.tsx @@ -1,5 +1,4 @@ import clsx from "clsx"; -import { motion } from "framer-motion"; import React from "react"; type IconType = (props: React.ComponentProps<"svg">) => JSX.Element; @@ -19,9 +18,7 @@ export const Toggle: React.FC = ({ }) => { const CurrentIcon = status ? OnIcon : OffIcon; return ( - = ({ )} onClick={() => onChange(!status)} > - +
- - - {status ? onText : offText} - - +
+ {status ? onText : offText} + ); }; From 3bdc5c67a2a6ef44e5c1de6d79189af0054514e9 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Jul 2022 12:12:03 +0400 Subject: [PATCH 2/6] spinner in autorun mode; autorun refactorings --- .../src/components/SquigglePlayground.tsx | 48 ++-------- .../components/src/components/ui/Toggle.tsx | 8 +- packages/components/src/lib/hooks/index.ts | 3 + .../src/lib/hooks/useMaybeControlledValue.ts | 22 +++++ .../src/lib/hooks/useRunnerState.ts | 92 +++++++++++++++++++ .../lib/{hooks.ts => hooks/useSquiggle.ts} | 22 +---- 6 files changed, 129 insertions(+), 66 deletions(-) create mode 100644 packages/components/src/lib/hooks/index.ts create mode 100644 packages/components/src/lib/hooks/useMaybeControlledValue.ts create mode 100644 packages/components/src/lib/hooks/useRunnerState.ts rename packages/components/src/lib/{hooks.ts => hooks/useSquiggle.ts} (65%) diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 5ca84040..a2fadb7d 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -1,7 +1,7 @@ import React, { FC, useState, useEffect, useMemo } from "react"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; import * as yup from "yup"; -import { useMaybeControlledValue } from "../lib/hooks"; +import { useMaybeControlledValue, useRunnerState } from "../lib/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; import { ChartSquareBarIcon, @@ -358,54 +358,18 @@ const RunControls: React.FC<{ )} ); }; -const useRunnerState = (code: string) => { - const [autorunMode, setAutorunMode] = useState(true); - const [renderedCode, setRenderedCode] = useState(code); // used in manual run mode only - const [isRunning, setIsRunning] = useState(false); // used in manual run mode only - - // This part is tricky and fragile; we need to re-render first to make sure that the icon is spinning, - // and only then evaluate the squiggle code (which freezes the UI). - // Also note that `useEffect` execution order matters here. - // Hopefully it'll all go away after we make squiggle code evaluation async. - useEffect(() => { - if (renderedCode === code && isRunning) { - // It's not possible to put this after `setRenderedCode(code)` below because React would apply - // `setIsRunning` and `setRenderedCode` together and spinning icon will disappear immediately. - setIsRunning(false); - } - }, [renderedCode, code, isRunning]); - - useEffect(() => { - if (!autorunMode && isRunning) { - setRenderedCode(code); // TODO - force run even if code hasn't changed - } - }, [autorunMode, code, isRunning]); - - const run = () => { - // The rest will be handled by useEffects above, but we need to update the spinner first. - setIsRunning(true); - }; - - return { - run, - renderedCode: autorunMode ? code : renderedCode, - isRunning, - autorunMode, - setAutorunMode: (newValue: boolean) => { - if (!newValue) setRenderedCode(code); - setAutorunMode(newValue); - }, - }; -}; - export const SquigglePlayground: FC = ({ defaultCode = "", height = 500, diff --git a/packages/components/src/components/ui/Toggle.tsx b/packages/components/src/components/ui/Toggle.tsx index 3295bda0..17ecc3f6 100644 --- a/packages/components/src/components/ui/Toggle.tsx +++ b/packages/components/src/components/ui/Toggle.tsx @@ -8,13 +8,15 @@ type Props = { onChange: (status: boolean) => void; texts: [string, string]; icons: [IconType, IconType]; + spinIcon?: boolean; }; export const Toggle: React.FC = ({ - texts: [onText, offText], - icons: [OnIcon, OffIcon], status, onChange, + texts: [onText, offText], + icons: [OnIcon, OffIcon], + spinIcon, }) => { const CurrentIcon = status ? OnIcon : OffIcon; return ( @@ -28,7 +30,7 @@ export const Toggle: React.FC = ({ onClick={() => onChange(!status)} >
- +
{status ? onText : offText} diff --git a/packages/components/src/lib/hooks/index.ts b/packages/components/src/lib/hooks/index.ts new file mode 100644 index 00000000..01fb46f9 --- /dev/null +++ b/packages/components/src/lib/hooks/index.ts @@ -0,0 +1,3 @@ +export { useMaybeControlledValue } from "./useMaybeControlledValue"; +export { useSquiggle, useSquigglePartial } from "./useSquiggle"; +export { useRunnerState } from "./useRunnerState"; diff --git a/packages/components/src/lib/hooks/useMaybeControlledValue.ts b/packages/components/src/lib/hooks/useMaybeControlledValue.ts new file mode 100644 index 00000000..aa7abc50 --- /dev/null +++ b/packages/components/src/lib/hooks/useMaybeControlledValue.ts @@ -0,0 +1,22 @@ +import { useState } from "react"; + +type ControlledValueArgs = { + value?: T; + defaultValue: T; + onChange?: (x: T) => void; +}; + +export function useMaybeControlledValue( + args: ControlledValueArgs +): [T, (x: T) => void] { + let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue); + let value = args.value ?? uncontrolledValue; + let onChange = (newValue: T) => { + if (args.value === undefined) { + // uncontrolled mode + setUncontrolledValue(newValue); + } + args.onChange?.(newValue); + }; + return [value, onChange]; +} diff --git a/packages/components/src/lib/hooks/useRunnerState.ts b/packages/components/src/lib/hooks/useRunnerState.ts new file mode 100644 index 00000000..e12eb5ad --- /dev/null +++ b/packages/components/src/lib/hooks/useRunnerState.ts @@ -0,0 +1,92 @@ +import { useEffect, useReducer } from "react"; + +type State = { + autorunMode: boolean; + renderedCode: string; + isRunning: boolean; + executionId: number; +}; + +const buildInitialState = (code: string) => ({ + autorunMode: true, + renderedCode: code, + isRunning: false, + executionId: 0, +}); + +type Action = + | { + type: "SET_AUTORUN_MODE"; + value: boolean; + code: string; + } + | { + type: "PREPARE_RUN"; + } + | { + type: "RUN"; + code: string; + } + | { + type: "STOP_RUN"; + }; + +const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "SET_AUTORUN_MODE": + return { + ...state, + autorunMode: action.value, + }; + case "PREPARE_RUN": + return { + ...state, + isRunning: true, + }; + case "RUN": + return { + ...state, + renderedCode: action.code, + executionId: state.executionId + 1, + }; + case "STOP_RUN": + return { + ...state, + isRunning: false, + }; + } +}; + +export const useRunnerState = (code: string) => { + const [state, dispatch] = useReducer(reducer, buildInitialState(code)); + + useEffect(() => { + if (state.isRunning) { + if (state.renderedCode !== code) { + dispatch({ type: "RUN", code }); + } else { + dispatch({ type: "STOP_RUN" }); + } + } + }, [state.isRunning, state.renderedCode, code]); + + const run = () => { + // The rest will be handled by dispatches above on following renders, but we need to update the spinner first. + dispatch({ type: "PREPARE_RUN" }); + }; + + if (state.autorunMode && state.renderedCode !== code && !state.isRunning) { + run(); + } + + return { + run, + autorunMode: state.autorunMode, + renderedCode: state.renderedCode, + isRunning: state.isRunning, + executionId: state.executionId, + setAutorunMode: (newValue: boolean) => { + dispatch({ type: "SET_AUTORUN_MODE", value: newValue, code }); + }, + }; +}; diff --git a/packages/components/src/lib/hooks.ts b/packages/components/src/lib/hooks/useSquiggle.ts similarity index 65% rename from packages/components/src/lib/hooks.ts rename to packages/components/src/lib/hooks/useSquiggle.ts index b52c23be..03b8c69d 100644 --- a/packages/components/src/lib/hooks.ts +++ b/packages/components/src/lib/hooks/useSquiggle.ts @@ -5,7 +5,7 @@ import { run, runPartial, } from "@quri/squiggle-lang"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo } from "react"; type SquiggleArgs> = { code: string; @@ -42,23 +42,3 @@ export const useSquigglePartial = ( export const useSquiggle = (args: SquiggleArgs>) => { return useSquiggleAny(args, run); }; - -type ControlledValueArgs = { - value?: T; - defaultValue: T; - onChange?: (x: T) => void; -}; -export function useMaybeControlledValue( - args: ControlledValueArgs -): [T, (x: T) => void] { - let [uncontrolledValue, setUncontrolledValue] = useState(args.defaultValue); - let value = args.value ?? uncontrolledValue; - let onChange = (newValue: T) => { - if (args.value === undefined) { - // uncontrolled mode - setUncontrolledValue(newValue); - } - args.onChange?.(newValue); - }; - return [value, onChange]; -} From eacc1adf1d507cbb380e1f3fee06c968bf99d019 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Jul 2022 13:03:23 +0400 Subject: [PATCH 3/6] executionId for code re-runs --- .../src/components/SquiggleChart.tsx | 3 ++ .../src/components/SquigglePlayground.tsx | 11 ++++-- .../src/lib/hooks/useRunnerState.ts | 34 +++++++++++-------- .../components/src/lib/hooks/useSquiggle.ts | 11 +++++- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 10e4e22c..088b49bf 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -14,6 +14,8 @@ import { SquiggleViewer } from "./SquiggleViewer"; export interface SquiggleChartProps { /** The input string for squiggle */ code?: string; + /** Allows to re-run the code if code hasn't changed */ + executionId?: number; /** If the output requires monte carlo sampling, the amount of samples */ sampleCount?: number; /** The amount of points returned to draw the distribution */ @@ -59,6 +61,7 @@ const defaultOnChange = () => {}; export const SquiggleChart: React.FC = React.memo( ({ code = "", + executionId = 0, environment, onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here height = 200, diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index a40b1efd..c7e63f4e 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -269,12 +269,19 @@ export const SquigglePlayground: FC = ({ [vars.sampleCount, vars.xyPointLength] ); - const { run, autorunMode, setAutorunMode, isRunning, renderedCode } = - useRunnerState(code); + const { + run, + autorunMode, + setAutorunMode, + isRunning, + renderedCode, + executionId, + } = useRunnerState(code); const squiggleChart = ( ({ +const buildInitialState = (code: string): State => ({ autorunMode: true, renderedCode: code, - isRunning: false, - executionId: 0, + runningState: "none", + executionId: 1, }); type Action = @@ -41,18 +42,19 @@ const reducer = (state: State, action: Action): State => { case "PREPARE_RUN": return { ...state, - isRunning: true, + runningState: "prepared", }; case "RUN": return { ...state, + runningState: "run", renderedCode: action.code, executionId: state.executionId + 1, }; case "STOP_RUN": return { ...state, - isRunning: false, + runningState: "none", }; } }; @@ -61,21 +63,23 @@ export const useRunnerState = (code: string) => { const [state, dispatch] = useReducer(reducer, buildInitialState(code)); useEffect(() => { - if (state.isRunning) { - if (state.renderedCode !== code) { - dispatch({ type: "RUN", code }); - } else { - dispatch({ type: "STOP_RUN" }); - } + if (state.runningState === "prepared") { + dispatch({ type: "RUN", code }); + } else if (state.runningState === "run") { + dispatch({ type: "STOP_RUN" }); } - }, [state.isRunning, state.renderedCode, code]); + }, [state.runningState, code]); const run = () => { // The rest will be handled by dispatches above on following renders, but we need to update the spinner first. dispatch({ type: "PREPARE_RUN" }); }; - if (state.autorunMode && state.renderedCode !== code && !state.isRunning) { + if ( + state.autorunMode && + state.renderedCode !== code && + state.runningState === "none" + ) { run(); } @@ -83,7 +87,7 @@ export const useRunnerState = (code: string) => { run, autorunMode: state.autorunMode, renderedCode: state.renderedCode, - isRunning: state.isRunning, + isRunning: state.runningState !== "none", executionId: state.executionId, setAutorunMode: (newValue: boolean) => { dispatch({ type: "SET_AUTORUN_MODE", value: newValue, code }); diff --git a/packages/components/src/lib/hooks/useSquiggle.ts b/packages/components/src/lib/hooks/useSquiggle.ts index 03b8c69d..4165a7be 100644 --- a/packages/components/src/lib/hooks/useSquiggle.ts +++ b/packages/components/src/lib/hooks/useSquiggle.ts @@ -9,6 +9,7 @@ import { useEffect, useMemo } from "react"; type SquiggleArgs> = { code: string; + executionId?: number; bindings?: bindings; jsImports?: jsImports; environment?: environment; @@ -21,7 +22,15 @@ const useSquiggleAny = >( ) => { const result: T = useMemo( () => f(args.code, args.bindings, args.environment, args.jsImports), - [f, args.code, args.bindings, args.environment, args.jsImports] + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + f, + args.code, + args.bindings, + args.environment, + args.jsImports, + args.executionId, + ] ); const { onChange } = args; From 329bb9432e98b73f99a56d6f4e47854e6f9d136b Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Jul 2022 21:28:21 +0400 Subject: [PATCH 4/6] playground renders code on second pass --- .../src/components/SquiggleChart.tsx | 1 + .../src/components/SquigglePlayground.tsx | 23 ++++++++++--------- .../src/lib/hooks/useRunnerState.ts | 12 ++++++---- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 088b49bf..86b2b078 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -88,6 +88,7 @@ export const SquiggleChart: React.FC = React.memo( environment, jsImports, onChange, + executionId, }); const distributionPlotSettings = { diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index c7e63f4e..2477f22f 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -278,17 +278,18 @@ export const SquigglePlayground: FC = ({ executionId, } = useRunnerState(code); - const squiggleChart = ( - - ); + const squiggleChart = + renderedCode === "" ? null : ( + + ); const firstTab = vars.showEditor ? (
diff --git a/packages/components/src/lib/hooks/useRunnerState.ts b/packages/components/src/lib/hooks/useRunnerState.ts index 861861d8..ff148d69 100644 --- a/packages/components/src/lib/hooks/useRunnerState.ts +++ b/packages/components/src/lib/hooks/useRunnerState.ts @@ -1,4 +1,4 @@ -import { useEffect, useReducer } from "react"; +import { useLayoutEffect, useReducer } from "react"; type State = { autorunMode: boolean; @@ -10,7 +10,7 @@ type State = { const buildInitialState = (code: string): State => ({ autorunMode: true, - renderedCode: code, + renderedCode: "", runningState: "none", executionId: 1, }); @@ -62,9 +62,13 @@ const reducer = (state: State, action: Action): State => { export const useRunnerState = (code: string) => { const [state, dispatch] = useReducer(reducer, buildInitialState(code)); - useEffect(() => { + useLayoutEffect(() => { if (state.runningState === "prepared") { - dispatch({ type: "RUN", code }); + // this is necessary for async playground loading - otherwise it executes the code synchronously on the initial load + // (it's surprising that this is necessary, but empirically it _is_ necessary, both with `useEffect` and `useLayoutEffect`) + setTimeout(() => { + dispatch({ type: "RUN", code }); + }, 0); } else if (state.runningState === "run") { dispatch({ type: "STOP_RUN" }); } From b1e7164c7e17a8221f0ca357d62f1b4cb0c62bcc Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Jul 2022 22:37:46 +0400 Subject: [PATCH 5/6] delayed overlay and autorun spinner --- .../src/components/SquigglePlayground.tsx | 28 ++++++++++--------- .../components/src/components/ui/Toggle.tsx | 13 +++++++-- packages/components/tailwind.config.js | 23 ++++++++++++++- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 2477f22f..79cac032 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -188,10 +188,7 @@ const RunControls: React.FC<{ )} = ({ const squiggleChart = renderedCode === "" ? null : ( - +
+ {isRunning ? ( +
+ ) : null} + +
); const firstTab = vars.showEditor ? ( diff --git a/packages/components/src/components/ui/Toggle.tsx b/packages/components/src/components/ui/Toggle.tsx index 17ecc3f6..bbf9a5ee 100644 --- a/packages/components/src/components/ui/Toggle.tsx +++ b/packages/components/src/components/ui/Toggle.tsx @@ -1,3 +1,4 @@ +import { RefreshIcon } from "@heroicons/react/solid"; import clsx from "clsx"; import React from "react"; @@ -29,8 +30,16 @@ export const Toggle: React.FC = ({ )} onClick={() => onChange(!status)} > -
- +
+ + {spinIcon && ( + + )}
{status ? onText : offText} diff --git a/packages/components/tailwind.config.js b/packages/components/tailwind.config.js index f059a98e..b49e3366 100644 --- a/packages/components/tailwind.config.js +++ b/packages/components/tailwind.config.js @@ -5,6 +5,27 @@ module.exports = { }, important: ".squiggle", theme: { - extend: {}, + extend: { + animation: { + "appear-and-spin": + "spin 1s linear infinite, squiggle-appear 0.2s forwards", + "semi-appear": "squiggle-semi-appear 0.2s forwards", + hide: "squiggle-hide 0.2s forwards", + }, + keyframes: { + "squiggle-appear": { + from: { opacity: 0 }, + to: { opacity: 1 }, + }, + "squiggle-semi-appear": { + from: { opacity: 0 }, + to: { opacity: 0.5 }, + }, + "squiggle-hide": { + from: { opacity: 1 }, + to: { opacity: 0 }, + }, + }, + }, }, }; From ec4777bd61e0f0812b7c6754adf26e776841f3ed Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Wed, 27 Jul 2022 23:01:15 +0400 Subject: [PATCH 6/6] don't spin pause icon on runs --- packages/components/src/components/SquigglePlayground.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 79cac032..837c4bed 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -191,7 +191,7 @@ const RunControls: React.FC<{ icons={[CheckCircleIcon, PauseIcon]} status={autorunMode} onChange={onAutorunModeChange} - spinIcon={isRunning} + spinIcon={autorunMode && isRunning} />
);