import "../index.css"; import "./App.css"; import { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { connectionsEqual, detectConnections, reducedConcreteSyntaxEqual } from "@/statecharts/detect_connections"; import { parseStatechart } from "../statecharts/parser"; import { BottomPanel } from "./BottomPanel/BottomPanel"; import { defaultSideBarState, SideBar, SideBarState } from "./SideBar/SideBar"; import { InsertMode } from "./TopPanel/InsertModes"; import { TopPanel } from "./TopPanel/TopPanel"; import { VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor"; import { makeAllSetters } from "./makePartialSetter"; import { useEditor } from "./hooks/useEditor"; import { useSimulator } from "./hooks/useSimulator"; import { useUrlHashState } from "../hooks/useUrlHashState"; import { plants } from "./plants"; import { emptyState } from "@/statecharts/concrete_syntax"; import { ModalOverlay } from "./Overlays/ModalOverlay"; import { FindReplace } from "./BottomPanel/FindReplace"; import { useCustomMemo } from "@/hooks/useCustomMemo"; export type EditHistory = { current: VisualEditorState, history: VisualEditorState[], future: VisualEditorState[], } export type AppState = { showKeys: boolean, zoom: number, insertMode: InsertMode, showFindReplace: boolean, findText: string, replaceText: string, } & SideBarState; const defaultAppState: AppState = { showKeys: true, zoom: 1, insertMode: 'and', showFindReplace: false, findText: "", replaceText: "", ...defaultSideBarState, } export type LightMode = "light" | "auto" | "dark"; export function App() { const [editHistory, setEditHistory] = useState(null); const [modal, setModal] = useState(null); const {commitState, replaceState, onRedo, onUndo, onRotate} = useEditor(setEditHistory); const editorState = editHistory && editHistory.current; const setEditorState = useCallback((cb: (value: VisualEditorState) => VisualEditorState) => { setEditHistory(historyState => historyState && ({...historyState, current: cb(historyState.current)})); }, [setEditHistory]); // parse concrete syntax always: const conns = useMemo(() => editorState && detectConnections(editorState), [editorState]); const parsed = useCustomMemo(() => editorState && conns && parseStatechart(editorState, conns), [editorState, conns] as const, // only parse again if anything changed to the connectedness / insideness... // parsing is fast, BUT re-rendering everything that depends on the AST is slow, and it's difficult to check if the AST changed because AST objects have recursive structure. ([prevState, prevConns], [nextState, nextConns]) => { if ((prevState === null) !== (nextState === null)) return false; if ((prevConns === null) !== (nextConns === null)) return false; // the following check is much cheaper than re-rendering everything that depends on return connectionsEqual(prevConns!, nextConns!) && reducedConcreteSyntaxEqual(prevState!, nextState!); }); const ast = parsed && parsed[0]; const [appState, setAppState] = useState(defaultAppState); const persist = useUrlHashState( recoveredState => { if (recoveredState === null) { setEditHistory(() => ({current: emptyState, history: [], future: []})); } // we support two formats // @ts-ignore else if (recoveredState.nextID) { // old format setEditHistory(() => ({current: recoveredState as VisualEditorState, history: [], future: []})); } else { // new format // @ts-ignore if (recoveredState.editorState !== undefined) { const {editorState, ...appState} = recoveredState as AppState & {editorState: VisualEditorState}; setEditHistory(() => ({current: editorState, history: [], future: []})); setAppState(defaultAppState => Object.assign({}, defaultAppState, appState)); } } }, ); useEffect(() => { const timeout = setTimeout(() => { if (editorState !== null) { console.log('persisting state to url'); persist({editorState, ...appState}); } }, 100); return () => clearTimeout(timeout); }, [editorState, appState]); const { autoScroll, plantConns, plantName, } = appState; const plant = plants.find(([pn, p]) => pn === plantName)![1]; const refRightSideBar = useRef(null); const scrollDownSidebar = useCallback(() => { if (autoScroll && refRightSideBar.current) { const el = refRightSideBar.current; // hack: we want to scroll to the new element, but we have to wait until it is rendered... setTimeout(() => { el.scrollIntoView({block: "end", behavior: "smooth"}); }, 50); } }, [refRightSideBar.current, autoScroll]); const simulator = useSimulator(ast, plant, plantConns, scrollDownSidebar); const setters = makeAllSetters(setAppState, Object.keys(appState) as (keyof AppState)[]); const syntaxErrors = parsed && parsed[1] || []; const currentTraceItem = simulator.trace && simulator.trace.trace[simulator.trace.idx]; const currentBigStep = currentTraceItem && currentTraceItem.kind === "bigstep" && currentTraceItem; const allErrors = [ ...syntaxErrors, ...(currentTraceItem && currentTraceItem.kind === "error") ? [{ message: currentTraceItem.error.message, shapeUid: currentTraceItem.error.highlight[0], }] : [], ]; const highlightActive = (currentBigStep && currentBigStep.state.sc.mode) || new Set(); const highlightTransitions = currentBigStep && currentBigStep.state.sc.firedTransitions || []; const plantState = useMemo(() => currentBigStep && currentBigStep.state.plant || plant.execution.initial()[1], [currentBigStep, plant]); return
{/* top-to-bottom: everything -> bottom panel */}
{/* left-to-right: main -> sidebar */}
{/* top-to-bottom: top bar, editor */}
{/* Top bar */}
{editHistory && }
{/* Editor */}
{editorState && conns && syntaxErrors && }
{appState.showFindReplace &&
setters.setShowFindReplace(false)}/>
}
{/* Right: sidebar */}
{/* Bottom panel */}
{syntaxErrors && }
; } export default App;