playground: manual play mode

This commit is contained in:
Vyacheslav Matyukhin 2022-06-26 21:15:09 +03:00
parent 2c2f299e46
commit 9208330038
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
3 changed files with 180 additions and 67 deletions

View File

@ -48,7 +48,8 @@ export interface SquiggleChartProps {
const defaultOnChange = () => {};
export const SquiggleChart: React.FC<SquiggleChartProps> = ({
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
({
code = "",
environment,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
@ -101,4 +102,5 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
environment={environment ?? defaultEnvironment}
/>
);
};
}
);

View File

@ -1,4 +1,4 @@
import React, { FC, Fragment, useState, useEffect } from "react";
import React, { FC, Fragment, useState, useEffect, useMemo } from "react";
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup";
import { useMaybeControlledValue } from "../lib/hooks";
@ -6,10 +6,14 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Tab } from "@headlessui/react";
import {
ChartSquareBarIcon,
CheckCircleIcon,
CodeIcon,
CogIcon,
CurrencyDollarIcon,
EyeIcon,
PauseIcon,
PlayIcon,
RefreshIcon,
} from "@heroicons/react/solid";
import clsx from "clsx";
@ -20,6 +24,7 @@ import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert";
import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle";
interface PlaygroundProps {
/** The initial squiggle string to put in the playground */
@ -110,7 +115,7 @@ type StyledTabProps = {
const StyledTab: React.FC<StyledTabProps> = ({ name, icon: Icon }) => {
return (
<Tab key={name} as={Fragment}>
<Tab as={Fragment}>
{({ selected }) => (
<button className="group flex rounded-md focus:outline-none focus-visible:ring-offset-gray-100">
<span
@ -204,6 +209,53 @@ function Checkbox<T>({
);
}
const PlayControls: React.FC<{
autoplay: boolean;
isStale: boolean;
onAutoplayChange: (value: boolean) => void;
onPlay: () => void;
}> = ({ autoplay, isStale, onAutoplayChange, onPlay }) => {
const [playing, setPlaying] = useState(false);
const play = () => {
setPlaying(true);
};
// this is tricky; we need to render PlayControls first to make sure that the icon is spinning,
// and only then call onPlay (which freezes the UI)
useEffect(() => {
if (!autoplay && playing) {
onPlay();
setPlaying(false);
}
// don't add onPlay to the deps below
}, [autoplay, playing]); // eslint-disable-line react-hooks/exhaustive-deps
const CurrentPlayIcon = playing ? RefreshIcon : PlayIcon;
return (
<div className="flex space-x-1 items-center">
{autoplay ? null : (
<button onClick={play}>
<CurrentPlayIcon
className={clsx(
"w-8 h-8",
playing && "animate-spin",
isStale ? "text-indigo-500" : "text-gray-400"
)}
/>
</button>
)}
<Toggle
texts={["Autoplay", "Paused"]}
icons={[CheckCircleIcon, PauseIcon]}
status={autoplay}
onChange={onAutoplayChange}
/>
</div>
);
};
export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "",
height = 500,
@ -225,7 +277,14 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const [importString, setImportString] = useState("{}");
const [imports, setImports] = useState({});
const [importsAreValid, setImportsAreValid] = useState(true);
const { register, control } = useForm({
const [renderedCode, setRenderedCode] = useState(""); // used only if autoplay is false
const {
register,
control,
setValue: setFormValue,
} = useForm({
resolver: yupResolver(schema),
defaultValues: {
sampleCount: 1000,
@ -242,6 +301,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
diagramStart: 0,
diagramStop: 10,
diagramCount: 20,
autoplay: true,
},
});
const vars = useWatch({
@ -252,10 +312,14 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSettingsChange?.(vars);
}, [vars, onSettingsChange]);
const env: environment = {
const env: environment = useMemo(
() => ({
sampleCount: Number(vars.sampleCount),
xyPointLength: Number(vars.xyPointLength),
};
}),
[vars.sampleCount, vars.xyPointLength]
);
const getChangeJson = (r: string) => {
setImportString(r);
try {
@ -418,7 +482,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const squiggleChart = (
<SquiggleChart
code={code}
code={vars.autoplay ? code : renderedCode}
environment={env}
diagramStart={Number(vars.diagramStart)}
diagramStop={Number(vars.diagramStop)}
@ -470,7 +534,8 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<SquiggleContainer>
<Tab.Group>
<div className="pb-4">
<Tab.List className="flex w-fit p-0.5 mt-2 rounded-md bg-slate-100 hover:bg-slate-200">
<div className="flex justify-between items-center mt-2">
<Tab.List className="flex w-fit p-0.5 rounded-md bg-slate-100 hover:bg-slate-200">
<StyledTab
name={vars.showEditor ? "Code" : "Display"}
icon={vars.showEditor ? CodeIcon : EyeIcon}
@ -479,6 +544,18 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<StyledTab name="View Settings" icon={ChartSquareBarIcon} />
<StyledTab name="Input Variables" icon={CurrencyDollarIcon} />
</Tab.List>
<PlayControls
autoplay={vars.autoplay || false}
isStale={!vars.autoplay && renderedCode !== code}
onPlay={() => {
setRenderedCode(code);
}}
onAutoplayChange={(newValue) => {
if (!newValue) setRenderedCode(code);
setFormValue("autoplay", newValue);
}}
/>
</div>
{vars.showEditor ? withEditor : withoutEditor}
</div>
</Tab.Group>

View File

@ -0,0 +1,34 @@
import clsx from "clsx";
import React from "react";
type IconType = (props: React.ComponentProps<"svg">) => JSX.Element;
type Props = {
status: boolean;
onChange: (status: boolean) => void;
texts: [string, string];
icons: [IconType, IconType];
};
export const Toggle: React.FC<Props> = ({
texts: [onText, offText],
icons: [OnIcon, OffIcon],
status,
onChange,
}) => {
const CurrentIcon = status ? OnIcon : OffIcon;
return (
<button
className={clsx(
"rounded-full py-1 bg-indigo-500 text-white text-xs font-semibold flex items-center space-x-1",
status ? "bg-indigo-500" : "bg-gray-400",
status ? "pl-1 pr-3" : "pl-3 pr-1",
!status && "flex-row-reverse space-x-reverse"
)}
onClick={() => onChange(!status)}
>
<CurrentIcon className="w-6 h-6" />
<span>{status ? onText : offText}</span>
</button>
);
};